Compare commits

...

23 Commits
clia ... clib

Author SHA1 Message Date
Simon Glass
39290ca4cd pager: doc: Add mention of the pager feature
Add a note about the pager to the console documentation, with some more
information in the developer section. Also include the pager API.

Series-to: concept
Cover-letter:
Introduce a pager for the console
Some U-Boot commands can produce a lot of output and this can run off
top of the display. This series introduces a simple pager feature, to
allow the output to be read, before pressing SPACE to continue.

A fixed buffer size (4K) is used, which sets a limit on the permmited
output before paging fails and the output is simply written all in one
lot. In most cases this is plenty, but 'env print' does print the whole
environment as one string, so if the legacy distro-boot scripts are used
it could happen.

The default number of visible lines is set with a Kconfig option. Where
a video terminal is being used, the page size is set to match that. In
general U-Boot cannot guess the number of visible lines, since a serial
terminal may be in use.

There is also a 'pager' environment variable which can override any
detected value.

This initial series only counts newlines. It has no support for counting
characters so the result will be sub-optimal if very long lines are
written. This could be addressed in a future patch.

Some effort is made to handle common cases correctly. For example, if
stdout is changed to a serial or vidconsole device, that is used to
determine the page length. When redirecting sandbox to a file, no
attempt is made.

Future work may detect the terminal size by sending an ANSI sequence.
END

Signed-off-by: Simon Glass <sjg@chromium.org>
Series-version: 2
2025-08-25 20:33:53 -06:00
Simon Glass
cfad2cabd3 efi: Enable the console pager for the app
The app can produce quite a bit of output, so enable the pager feature.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
0ae6a74826 console: Add tests for calc_check_console_lines()
Add tests for checking:
- environment variable override with hex values
- invalid environment variable handling
- Kconfig-default behavior with device detection
- precedence rules (environment overrides device detection)
- serial terminal detection by manipulating sandbox state

Also expose calc_check_console_lines() in console.h for testing
and update pager functionality to use bypass mode when serial
is not connected to a terminal.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
c00ce7e4fd console: Recheck the page length when stdout device changes
It is annoying to have the pager use the line count from the display
when console output is actually using serial. Check this when stdio
changes and recaculate as needed.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
4dbff9414e console: Get the pager lines from the serial console
If there is a serial console, use that to set the number of lines for the
pager.

This is controlled by CONFIG_SERIAL_TERM_PRESENT

Enable SERIAL_TERM_PRESENT by default if the pager is being used, since
people generally have a serial console enabled.

Make the serial a lower priority than the vidconsole.

We don't want to use the pager when the output of sandbox is being
redirected. So in that case, put the pager in bypass mode.

Series-changes: 2
- Only use the vidconsole if we believe it is visible

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
5a316de7eb console: Get the pager lines from the vidconsole
If there is a video console, use that to set the number of lines for the
pager.

For sandbox, only do this if the -l flag was provided, i.e. the emulated
LCD is visible.

Series-changes: 2
- Only use the vidconsole if we believe it is visible

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
3df7ef8813 bdinfo: Show the pager page_len value
Show the actual value being used by the pager, if enabled.

Series-changes: 2
- Add new patch to show the pager page_len value

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
89f7699c81 console: Implement an environment var to control the pager
The 'pager' environment variable can be used to set the number of lines
on the display. Provide a callback for this.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
13a69d6460 console: Reset the pager when entering a new command
We don't want the pager appearing when entering commands, so add a way
to bypass it. Reset the line count to zero before executing the command.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
7d82e5c79b console: Add a few more paging tests
Provide a test that the prompt is displayed and another that paging
happens but does not appear in the recorded console-output.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
88549138f6 console: Support paging with single characters
A single character may result in a stall waiting for user input, which
means that it may request that a string be output. So when the pager is
active we never actually use the devices' putc() methods. Add a special
case so they don't go to wrack and ruin.

As before, the pager is only supported with CONFIG_CONSOLE_MUX enabled.

Series-changes: 2
- Drop unnecessary '!= NULL'

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
831e273881 console: Support a bypass state
Allow the pager to be put in a bypass state, e.g. if not connected to
a terminal.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
107c045dde console: test: Allow tests to bypass the pager
We generally don't want the pager to be active when running tests,
since U-Boot appears to hang forever. Perhaps we could detect when the
tests are being run interactively and use the pager in that case. But
for now, just bypass it.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
e1375a5783 console: Plumb in the pager
Send all console output via the pager. If it is disabled, it will do
nothing.

The pager is only supported if CONSOLE_MUX and SYS_CONSOLE_IS_IN_ENV are
enabled. This is the common case for more richly featured boards, i.e.
those that can cope with the extra code size of this feature.

Series-changes: 2
- Drop an unnecessary direct call to the pager
- Repeat the old code in console_puts_pager() to avoid 16-byte growth

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:33:53 -06:00
Simon Glass
d84b68603c test/py: Disable terminal detection with sandbox tests
Detecting the terminal size results in unwanted output in tests. Use the
-A option to disable this.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 20:32:45 -06:00
Simon Glass
390bf2c3dd sandbox: Add -A option to assume no terminal present
Add a new -A sandbox command-line option that sets a flag to assume no
terminal is present. This causes serial_query_size() to return -ENOENT
immediately, similar to when CONFIG_SERIAL_TERM_PRESENT is disabled.

This option is useful for testing pager behavior in non-interactive
environments without needing to modify configuration options.

Co-developed-by: Claude <noreply@anthropic.com>
2025-08-25 20:32:05 -06:00
Simon Glass
f75ea56c11 test/py: Disable the pager when running sandbox tests
The pager gets in the way of most tests, since it kicks in (by default)
when a command results in more than 25 lines of output. Use the -P flag
to bypass it.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 15:18:16 -06:00
Simon Glass
06ff6888a1 sandbox: Add -P flag to enable bypassing the pager
Add a new -P command line flag to sandbox that enables pager-bypass
mode. This is useful for tests and automated scripts that need to
disable pager interrupts.

The implementation stores the bypass request in sandbox_state during
command line parsing, then applies it in sandbox_main_loop_init()
after the pager has been initialized.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 15:18:14 -06:00
Simon Glass
282f24e87d console: Rename console-put functions to prepare for pager
Rename console_puts() to console_puts_pager() and console_putc() to
console_putc_pager() to prepare for implementing a console-pager
feature.

All normal output goes through the pager.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 11:29:31 -06:00
Simon Glass
41726522d4 console: Provide a way to output without the pager
Sometimes output should be sent ignoring the pager, such as when it is
a message related to paging. Add a parameter to support this.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 11:29:31 -06:00
Simon Glass
363aa5cc8d console: Add tests for the basic functionality
Cover the various cases in the base code. Put the tests in the 'common'
suite to match where the pager implementation is.

Co-developed-by: Claude <noreply@anthropic.com>
Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 11:29:31 -06:00
Simon Glass
56483c8367 console: Add the basic pager implementation
Add an implementation of the pager. It has quite a simple API:

  - call pager_post() to send some output, which returns what should
    actually be sent to the console devices
  - then call pager_next() repeatedly, outputting what it returns,
    until it returns NULL

There is one special case: pager_next() returns PAGER_WAITING if it is
waiting for the user to press a key. In that case, the caller should
read a key and then pass it to the next pager_next() call.

Internally, there is a simple state machine to cope with hitting the
limit (and outputing a prompt), waiting for the user and the clearing
the prompt, before continuing in the normal PAGEST_OK state.

Signed-off-by: Simon Glass <sjg@chromium.org>
2025-08-25 11:29:30 -06:00
Simon Glass
a845243258 doc: Tidy up the console docs a little
Tweak the documentation to look better when viewed. Use 'write' instead
of 'put'.

Series-to: concept
Series-cc: heinrich
Series-version: 2
Cover-letter:
console: Refactor in preparation for the pager
This series tidies up a few small things the serial and console areas:

- Move detection of serial-console size to the serial module
- Provide a Kconfig to disable serial detection
- Fix some missing driver-model flags in stdio devices
- Move console docs into the main documentation

This series is marked v2 since some of the patches were sent in any
earlier series. With various tweaks that series grew too large to be
sent as a single series.
END

Signed-off-by: Simon Glass <sjg@chromium.org>
Series-links: 2:17
2025-08-25 10:27:02 -06:00
27 changed files with 1609 additions and 51 deletions

View File

@@ -12,6 +12,7 @@
#include <init.h>
#include <log.h>
#include <os.h>
#include <pager.h>
#include <sandbox_host.h>
#include <sort.h>
#include <spl.h>
@@ -136,6 +137,10 @@ int sandbox_main_loop_init(void)
{
struct sandbox_state *state = state_get_current();
/* Apply pager bypass if requested */
if (state->pager_bypass)
pager_set_test_bypass(gd_pager(), true);
/* Execute command if required */
if (state->cmd || state->run_distro_boot) {
int retval = 0;
@@ -433,6 +438,15 @@ static int sandbox_cmdline_cb_autoboot_keyed(struct sandbox_state *state,
}
SANDBOX_CMDLINE_OPT(autoboot_keyed, 0, "Allow keyed autoboot");
static int sandbox_cmdline_cb_no_term_present(struct sandbox_state *state,
const char *arg)
{
state->no_term_present = true;
return 0;
}
SANDBOX_CMDLINE_OPT_SHORT(no_term_present, 'A', 0,
"Assume no terminal present (for pager testing)");
static int sandbox_cmdline_cb_upl(struct sandbox_state *state, const char *arg)
{
state->upl = true;
@@ -480,6 +494,16 @@ static int sandbox_cmdline_cb_soft_fail(struct sandbox_state *state,
SANDBOX_CMDLINE_OPT_SHORT(soft_fail, 'f', 0,
"continue test execution even after it fails");
static int sandbox_cmdline_cb_pager_bypass(struct sandbox_state *state,
const char *arg)
{
state->pager_bypass = true;
return 0;
}
SANDBOX_CMDLINE_OPT_SHORT(pager_bypass, 'P', 0,
"Enable pager bypass mode");
static int sandbox_cmdline_cb_bind(struct sandbox_state *state, const char *arg)
{
if (state->num_binds >= SB_MAX_BINDS) {

View File

@@ -175,6 +175,8 @@ struct sandbox_state {
bool native; /* Adjust to reflect host arch */
bool no_flattree_tests; /* Don't run second set of DM tests */
bool soft_fail; /* Continue on failure */
bool pager_bypass; /* Enable pager-bypass mode */
bool no_term_present; /* Assume no terminal present */
/* Pointer to information for each SPI bus/cs */
struct sandbox_spi_info spi[CONFIG_SANDBOX_SPI_MAX_BUS]

View File

@@ -14,6 +14,7 @@
#include <lmb.h>
#include <mapmem.h>
#include <net.h>
#include <pager.h>
#include <serial.h>
#include <video.h>
#include <vsprintf.h>
@@ -132,6 +133,8 @@ static int bdinfo_print_all(struct bd_info *bd)
lprint_num_l("fdt_blob", (ulong)map_to_sysmem(gd->fdt_blob));
if (IS_ENABLED(CONFIG_VIDEO))
show_video_info();
if (IS_ENABLED(CONFIG_CONSOLE_PAGER))
printf("pager = %d\n", gd_pager_page_len());
#if CONFIG_IS_ENABLED(MULTI_DTB_FIT)
lprint_num_l("multi_dtb_fit", (ulong)gd->multi_dtb_fit);
#endif

View File

@@ -332,6 +332,28 @@ config SYS_DEVICE_NULLDEV
operation of the console by setting stdout to "nulldev". Enable
this to use a serial console under board control.
config CONSOLE_PAGER
bool "Enable console output paging"
depends on CONSOLE_MUX
imply CMD_CONSOLE_EXTRA
imply SERIAL_TERM_PRESENT
default y if SANDBOX
help
Enable pager functionality for console output. When enabled, long
output will be paused after a configurable number of lines, waiting
for user input (SPACE) to continue. The number of lines per page is
controlled by the 'pager' environment variable. If the variable is
not set or is empty, paging is disabled.
config CONSOLE_PAGER_LINES
int "Number of lines per page"
depends on CONSOLE_PAGER
default 25
help
Sets the default number of lines that the pager assumes is visible on
the display. This is used as a default if the "pager" environment
variable is unset.
endmenu
menu "Logging"

View File

@@ -26,6 +26,8 @@ obj-$(CONFIG_MII) += miiphyutil.o
obj-$(CONFIG_CMD_MII) += miiphyutil.o
obj-$(CONFIG_PHYLIB) += miiphyutil.o
obj-$(CONFIG_CONSOLE_PAGER) += pager.o
obj-$(CONFIG_USB_HOST) += usb.o usb_hub.o
obj-$(CONFIG_USB_GADGET) += usb.o
obj-$(CONFIG_USB_STORAGE) += usb_storage.o

View File

@@ -13,6 +13,7 @@
#include <command.h>
#include <hang.h>
#include <malloc.h>
#include <pager.h>
#include <time.h>
#include <watchdog.h>
#include <linux/errno.h>
@@ -650,6 +651,9 @@ int cli_readline_into_buffer(const char *const prompt, char *buffer,
uint len = CONFIG_SYS_CBSIZE;
int rc;
static int initted;
bool old_bypass;
old_bypass = pager_set_bypass(gd_pager(), true);
/*
* Say N to CMD_HISTORY_USE_CALLOC will skip runtime
@@ -673,9 +677,14 @@ int cli_readline_into_buffer(const char *const prompt, char *buffer,
puts(prompt);
rc = cread_line(prompt, p, &len, timeout);
return rc < 0 ? rc : len;
rc = rc < 0 ? rc : len;
} else {
return cread_line_simple(prompt, p);
rc = cread_line_simple(prompt, p);
}
pager_set_bypass(gd_pager(), old_bypass);
pager_reset(gd_pager());
return rc;
}

View File

@@ -16,6 +16,7 @@
#include <malloc.h>
#include <mapmem.h>
#include <os.h>
#include <pager.h>
#include <serial.h>
#include <stdio_dev.h>
#include <exports.h>
@@ -256,14 +257,22 @@ static int console_tstc(int file)
return 0;
}
static void console_putc(int file, const char c)
{
int i;
struct stdio_dev *dev;
static void console_puts_pager(int file, const char *s);
for_each_console_dev(i, file, dev) {
if (dev->putc != NULL)
dev->putc(dev, c);
static void console_putc_pager(int file, const char c)
{
if (IS_ENABLED(CONFIG_CONSOLE_PAGER) && gd_pager()) {
char str[2] = {c, '\0'};
console_puts_pager(file, str);
} else {
int i;
struct stdio_dev *dev;
for_each_console_dev(i, file, dev) {
if (dev->putc)
dev->putc(dev, c);
}
}
}
@@ -312,14 +321,39 @@ int console_printf_select_stderr(bool serial_only, const char *fmt, ...)
return ret;
}
static void console_puts(int file, const char *s)
static void console_puts(int file, bool use_pager, const char *s)
{
int i;
struct stdio_dev *dev;
int key = 0;
for_each_console_dev(i, file, dev) {
if (dev->puts != NULL)
dev->puts(dev, s);
for (s = pager_post(gd_pager(), use_pager, s); s;
s = pager_next(gd_pager(), use_pager, key)) {
struct stdio_dev *dev;
int i;
key = 0;
if (IS_ENABLED(CONFIG_CONSOLE_PAGER) && s == PAGER_WAITING) {
key = getchar();
} else if (*s) {
for_each_console_dev(i, file, dev) {
if (dev->puts != NULL)
dev->puts(dev, s);
}
}
}
}
static void console_puts_pager(int file, const char *s)
{
if (IS_ENABLED(CONFIG_CONSOLE_PAGER) && gd_pager()) {
console_puts(file, true, s);
} else {
struct stdio_dev *dev;
int i;
for_each_console_dev(i, file, dev) {
if (dev->puts != NULL)
dev->puts(dev, s);
}
}
}
@@ -342,7 +376,39 @@ static inline void console_doenv(int file, struct stdio_dev *dev)
iomux_doenv(file, dev->name);
}
#endif
#else
/**
* sdev_file_has_uclass() - Find a file has a device of a particular uclass
*
* Given a file, this returns the first device of the given uclass that is found
* within that file.
*
* This only works with driver model
*
* @file: File to check (e.g. stdout)
* @id: uclass ID to look for
* Return: device, if found, NULL if not
*/
static struct udevice *sdev_file_has_uclass(int file, enum uclass_id id)
{
struct stdio_dev *sdev;
int i;
for_each_console_dev(i, file, sdev) {
if (sdev->flags & DEV_FLAGS_DM) {
struct udevice *dev = sdev->priv;
if (device_get_uclass_id(dev) == id)
return dev;
} else if (id == UCLASS_SERIAL && console_dev_is_serial(sdev)) {
return gd->cur_serial_dev;
}
}
return NULL;
}
#else /* !CONSOLE_MUX */
static void console_devices_set(int file, struct stdio_dev *dev)
{
@@ -368,7 +434,7 @@ static inline int console_tstc(int file)
return stdio_devices[file]->tstc(stdio_devices[file]);
}
static inline void console_putc(int file, const char c)
static inline void console_putc_pager(int file, const char c)
{
stdio_devices[file]->putc(stdio_devices[file], c);
}
@@ -380,7 +446,7 @@ void console_puts_select(int file, bool serial_only, const char *s)
stdio_devices[file]->puts(stdio_devices[file], s);
}
static inline void console_puts(int file, const char *s)
static inline void console_puts_pager(int file, const char *s)
{
stdio_devices[file]->puts(stdio_devices[file], s);
}
@@ -399,8 +465,52 @@ static inline void console_doenv(int file, struct stdio_dev *dev)
console_setfile(file, dev);
}
#endif
static inline struct udevice *sdev_file_has_uclass(int file, enum uclass_id id)
{
return NULL;
}
#endif /* CONIFIG_IS_ENABLED(CONSOLE_MUX) */
int calc_check_console_lines(void)
{
int lines, dev_lines = -1;
struct udevice *dev;
lines = env_get_hex("pager", -1);
if (lines != -1)
return lines;
lines = IF_ENABLED_INT(CONFIG_CONSOLE_PAGER,
CONFIG_CONSOLE_PAGER_LINES);
/* get number of lines from the video console, if available */
if (IS_ENABLED(CONFIG_VIDEO) && video_is_visible()) {
dev = sdev_file_has_uclass(stdout, UCLASS_VIDEO_CONSOLE);
if (dev) {
struct vidconsole_priv *priv;
priv = dev_get_uclass_priv(dev);
dev_lines = priv->rows;
}
}
/* get number of lines from the serial console, if available */
if (IS_ENABLED(CONFIG_DM_SERIAL) &&
sdev_file_has_uclass(stdout, UCLASS_SERIAL)) {
int cols;
if (!serial_is_tty())
dev_lines = 0;
else if (dev_lines == -1)
serial_query_size(&dev_lines, &cols);
}
if (dev_lines != -1)
lines = dev_lines;
return lines;
}
static void __maybe_unused console_setfile_and_devices(int file, struct stdio_dev *dev)
{
console_setfile(file, dev);
@@ -497,13 +607,13 @@ int ftstc(int file)
void fputc(int file, const char c)
{
if ((unsigned int)file < MAX_FILES)
console_putc(file, c);
console_putc_pager(file, c);
}
void fputs(int file, const char *s)
{
if ((unsigned int)file < MAX_FILES)
console_puts(file, s);
console_puts_pager(file, s);
}
#ifdef CONFIG_CONSOLE_FLUSH_SUPPORT
@@ -1075,6 +1185,15 @@ static int on_console(const char *name, const char *value, enum env_op op,
break;
}
if (IS_ENABLED(CONFIG_CONSOLE_PAGER) && console == stdout) {
int lines = calc_check_console_lines();
/* Set bypass mode if not connected to a terminal */
pager_set_bypass(gd_pager(), lines != 0);
if (lines)
pager_set_page_len(gd_pager(), lines);
}
return result;
}
U_BOOT_ENV_CALLBACK(console, on_console);
@@ -1101,6 +1220,19 @@ static int on_silent(const char *name, const char *value, enum env_op op,
U_BOOT_ENV_CALLBACK(silent, on_silent);
#endif
static void setup_pager(void)
{
/* Init pager now that console is ready */
if (IS_ENABLED(CONFIG_CONSOLE_PAGER)) {
int ret;
ret = pager_init(gd_pagerp(), calc_check_console_lines(),
PAGER_BUF_SIZE);
if (ret)
printf("Failed to init pager\n");
}
}
#if CONFIG_IS_ENABLED(SYS_CONSOLE_IS_IN_ENV)
/* Called after the relocation - use desired console functions */
int console_init_r(void)
@@ -1185,6 +1317,7 @@ done:
}
gd->flags |= GD_FLG_DEVINIT; /* device initialization completed */
setup_pager();
print_pre_console_buffer(flushpoint);
return 0;
@@ -1252,6 +1385,7 @@ int console_init_r(void)
}
gd->flags |= GD_FLG_DEVINIT; /* device initialization completed */
setup_pager();
print_pre_console_buffer(flushpoint);
return 0;

219
common/pager.c Normal file
View File

@@ -0,0 +1,219 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Deals with splitting up text output into separate screenfuls
*
* Copyright 2025 Simon Glass <sjg@chromium.org>
*/
#define LOG_CATEGORY LOGC_CONSOLE
#include <env.h>
#include <errno.h>
#include <malloc.h>
#include <pager.h>
#include <asm/global_data.h>
DECLARE_GLOBAL_DATA_PTR;
const char *pager_post(struct pager *pag, bool use_pager, const char *s)
{
struct membuf old;
int ret, len;
if (!pag || !use_pager || pag->test_bypass || pag->state == PAGERST_BYPASS)
return s;
len = strlen(s);
if (!len)
return NULL;
old = pag->mb;
ret = membuf_put(&pag->mb, s, len);
if (ret == len) {
/* all is well */
} else {
/*
* We couldn't store any of the text, so we'll store none of
* it. The pager is now in an non-functional state until it
* can eject the overflow text.
*
* The buffer is presumably empty, since callers are not allowed
* to call pager_post() unless all the output from the previous
* call was provided via pager_next().
*/
pag->overflow = s;
pag->mb = old;
}
return pager_next(pag, true, 0);
}
const char *pager_next(struct pager *pag, bool use_pager, int key)
{
char *str, *p, *end;
int ret;
if (!use_pager || (pag && pag->test_bypass))
return NULL;
/* replace the real character we overwrite with nul, if needed */
if (pag->nulch) {
*pag->nulch = pag->oldch;
pag->nulch = NULL;
}
/* if we're at the limit, wait */
switch (pag->state) {
case PAGERST_OK:
break;
case PAGERST_AT_LIMIT:
pag->state = PAGERST_WAIT_USER;
return PAGER_PROMPT;
case PAGERST_WAIT_USER:
if (key != ' ')
return PAGER_WAITING;
pag->state = PAGERST_CLEAR_PROMPT;
return PAGER_BLANK;
case PAGERST_CLEAR_PROMPT:
pag->state = PAGERST_OK;
break;
case PAGERST_BYPASS:
return NULL;
}
ret = membuf_getraw(&pag->mb, pag->buf.size - 1, false, &str);
if (!ret) {
if (pag->overflow) {
const char *oflow = pag->overflow;
pag->overflow = NULL;
return oflow;
}
return NULL;
}
/* return lines until we reach the limit */
for (p = str, end = str + ret; p < end; p++) {
if (*p == '\n' && ++pag->line_count == pag->page_len - 1) {
/* remember to display the pager message next time */
pag->state = PAGERST_AT_LIMIT;
pag->line_count = 0;
/* skip the newline, since our prompt has one */
p++;
break;
}
}
/* remove the used bytes from the membuf */
ret = membuf_getraw(&pag->mb, p - str, true, &str);
/* don't output the newline, since our prompt has one */
if (pag->state == PAGERST_AT_LIMIT)
p--;
/* terminate the string */
pag->nulch = p;
pag->oldch = *pag->nulch;
*pag->nulch = '\0';
return str;
}
bool pager_set_bypass(struct pager *pag, bool bypass)
{
bool was_bypassed = false;
if (!pag)
return false;
was_bypassed = pag->state == PAGERST_BYPASS;
if (bypass)
pag->state = PAGERST_BYPASS;
else
pag->state = PAGERST_OK;
return was_bypassed;
}
bool pager_set_test_bypass(struct pager *pag, bool bypass)
{
bool was_bypassed = false;
if (!pag)
return false;
was_bypassed = pag->test_bypass;
pag->test_bypass = bypass;
return was_bypassed;
}
void pager_set_page_len(struct pager *pag, int page_len)
{
if (page_len < 2)
return;
pag->page_len = page_len;
pag->line_count = 0;
}
void pager_reset(struct pager *pag)
{
pag->line_count = 0;
}
static int on_pager(const char *name, const char *value, enum env_op op,
int flags)
{
struct pager *pag = gd_pager();
int new_page_len;
if (!IS_ENABLED(CONFIG_CONSOLE_PAGER) || !pag)
return 0;
switch (op) {
case env_op_create:
case env_op_overwrite:
if (value) {
new_page_len = simple_strtoul(value, NULL, 16);
pager_set_page_len(pag, new_page_len);
}
break;
case env_op_delete:
/* Reset to default when deleted */
pager_set_page_len(pag, CONFIG_CONSOLE_PAGER_LINES);
break;
}
return 0;
}
U_BOOT_ENV_CALLBACK(pager, on_pager);
void pager_uninit(struct pager *pag)
{
abuf_uninit(&pag->buf);
free(pag);
}
int pager_init(struct pager **pagp, int page_len, int buf_size)
{
struct pager *pag;
pag = malloc(sizeof(struct pager));
if (!pag)
return log_msg_ret("pag", -ENOMEM);
memset(pag, '\0', sizeof(struct pager));
pag->page_len = page_len;
if (!abuf_init_size(&pag->buf, buf_size))
return log_msg_ret("pah", -ENOMEM);
/*
* nul-terminate the buffer, which will come in handy if we need to
* return up to the last byte
*/
((char *)pag->buf.data)[buf_size - 1] = '\0';
membuf_init(&pag->mb, pag->buf.data, buf_size);
*pagp = pag;
return 0;
}

View File

@@ -20,6 +20,7 @@ U-Boot API documentation
lmb
logging
nvmem
pager
part
pinctrl
rng

10
doc/api/pager.rst Normal file
View File

@@ -0,0 +1,10 @@
.. SPDX-License-Identifier: GPL-2.0+
.. Copyright Simon Glass <sjg@chromium.org>
Console Pager
=============
See :doc:`/develop/console`.
.. kernel-doc:: include/pager.h
:internal:

41
doc/develop/console.rst Normal file
View File

@@ -0,0 +1,41 @@
.. SPDX-License-Identifier: GPL-2.0+
.. sectionauthor:: Simon Glass <sjg@chromium.org>
=======
Console
=======
Paging
------
See the user documentation at :doc:`/usage/console`. For the API, see
:doc:`/api/pager`.
The pager is implemented in `common/pager.c` and integrated into the console
system via `console_puts()` in `common/console.c`. When output is sent to
stdout:
1. Text is passed to `pager_post()` which buffers it
2. `pager_next()` returns portions of text up to the page limit
3. When the page limit is reached, a prompt is displayed
4. The system waits for user input (SPACE key) via `getchar()`
5. After SPACE is pressed, the prompt is cleared and output continues
The pager maintains state through the `struct pager` which tracks:
- Current line count within the page
- Page length setting
- Text buffer and overflow handling
- Current pager state (normal, waiting for user, clearing prompt)
Bypass Mode
~~~~~~~~~~~
The pager can be put into bypass mode using `pager_set_bypass()`, which is
useful for automated testing. In bypass mode, all text flows through without
any paging interruptions.
The pager automatically enters bypass mode when output is not connected to a
terminal, preventing paging prompts from appearing in non-interactive
environments like scripts or automated builds.

View File

@@ -35,6 +35,7 @@ Implementation
ci_testing
commands
config_binding
console
cyclic
devicetree/index
distro

View File

@@ -1,15 +1,16 @@
.. SPDX-License-Identifier: GPL-2.0+
.. sectionauthor:: Paolo Scaffardi, AIRVENT SAM s.p.a - RIMINI(ITALY), arsenio@tin.it
.. (C) Copyright 2000
.. sectionauthor:: Simon Glass <sjg@chromium.org>
=======================
U-Boot console handling
=======================
HOW THE CONSOLE WORKS?
----------------------
Introduction
------------
At system startup U-Boot initializes a serial console. When U-Boot
At system-startup U-Boot initializes a serial console. When U-Boot
relocates itself to RAM, all console drivers are initialized (they
will register all detected console devices to the system for further
use).
@@ -17,43 +18,90 @@ use).
If not defined in the environment, the first input device is assigned
to the 'stdin' file, the first output one to 'stdout' and 'stderr'.
You can use the command "coninfo" to see all registered console
You can use the command `coninfo` to see all registered console
devices and their flags. You can assign a standard file (stdin,
stdout or stderr) to any device you see in that list simply by
assigning its name to the corresponding environment variable. For
example:
example::
setenv stdin serial <- To use the serial input
setenv stdout video <- To use the video console
# Use the serial input
setenv stdin serial
Do a simple "saveenv" to save the console settings in the environment
# Use the video console
setenv stdout vidconsole
Do a simple `saveenv` to save the console settings in the environment
and get them working on the next startup, too.
HOW CAN I USE STANDARD FILE INTO THE SOURCES?
---------------------------------------------
How to output text to the console
---------------------------------
You can use the following functions to access the console:
* STDOUT:
putc (to put a char to stdout)
puts (to put a string to stdout)
printf (to format and put a string to stdout)
stdout
- putc() - write a char to stdout
- puts() - write a string to stdout
- printf() - format and write a string to stdout
* STDIN:
tstc (to test for the presence of a char in stdin)
getc (to get a char from stdin)
stdin
- tstc() - test for the presence of a char in stdin
- getchar() - get a char from stdin
* STDERR:
eputc (to put a char to stderr)
eputs (to put a string to stderr)
eprintf (to format and put a string to stderr)
stderr
- eputc() - write a char to stderr
- eputs() - write a string to stderr
- eprintf() - format and write a string to stderr
* FILE (can be 'stdin', 'stdout', 'stderr'):
fputc (like putc but redirected to a file)
fputs (like puts but redirected to a file)
fprintf (like printf but redirected to a file)
ftstc (like tstc but redirected to a file)
fgetc (like getc but redirected to a file)
file ('stdin', 'stdout' or 'stderr')
- fputc() - write a char to a file
- fputs() - write a string to a file
- fprintf() - format and write a string to a file
- ftstc() - test for the presence of a char in file
- fgetc() - get a char from a file
Remember that all FILE-related functions CANNOT be used before
U-Boot relocation (done in 'board_init_r' in `arch/*/lib/board.c`).
Remember that FILE-related functions CANNOT be used before U-Boot relocation,
which is done in `board_init_r()`.
Pager
-----
U-Boot has a simple pager feature, enabled with `CONFIG_CONSOLE_PAGER`. It is
only available if `CONFIG_CONSOLE_MUX` is also enabled.
When activated, the pager pauses at the end of each 'page' (screenful) of
output, shows a prompt ": Press SPACE to continue" and lets the user read the
output. To continue to the next page, press the SPACE key.
Page Size Configuration
~~~~~~~~~~~~~~~~~~~~~~~
The number of lines per page is determined in the following order of priority:
1. **Environment variable**: The `pager` environment variable (hex value)
takes highest priority. Set to 0 to disable paging.
2. **Video console detection**: If no environment variable is set and a video
console is active, the pager uses the number of rows from the video console.
3. **Serial TTY detection**: For serial consoles, the pager checks if the
output is connected to a terminal (TTY). If not connected to a TTY, paging
is disabled. This check works by sending a few special characters to the
terminal and (hopefully) receiving a reply. If you are logging the output of
U-Boot, you may see these characters in the log. Disable
`CONFIG_SERIAL_TERM_PRESENT` is this is unwanted.
4. **Configuration default**: If none of the above apply, falls back to
`CONFIG_CONSOLE_PAGER_LINES`.
Examples::
# Set page size to 30 lines (hex value 1e)
setenv pager 1e
# Set page size to 24 lines (hex value 18)
setenv pager 18
# Disable paging
setenv pager 0
For developer documentation, please see :doc:`/develop/console`.

View File

@@ -335,6 +335,10 @@ netretry
Useful on scripts which control the retry operation
themselves.
pager
Decimal number of visible lines on the display, or serial console.
:doc:`/usage/console`.
rng_seed_size
Size of random value added to device-tree node /chosen/rng-seed.
This variable is given as a decimal number.

View File

@@ -18,6 +18,9 @@
#include <asm/global_data.h>
#include <dm/lists.h>
#include <dm/device-internal.h>
#ifdef CONFIG_SANDBOX
#include <asm/state.h>
#endif
#include <dm/of_access.h>
#include <linux/build_bug.h>
#include <linux/delay.h>
@@ -647,6 +650,14 @@ int serial_query_size(int *rowsp, int *colsp)
if (!CONFIG_IS_ENABLED(SERIAL_TERM_PRESENT))
return -ENOENT;
#ifdef CONFIG_SANDBOX
if (IS_ENABLED(CONFIG_SANDBOX)) {
struct sandbox_state *state = state_get_current();
if (state->no_term_present)
return -ENOENT;
}
#endif
/* Empty input buffer */
while (tstc())
getchar();

View File

@@ -24,6 +24,7 @@
#include <event_internal.h>
#include <fdtdec.h>
#include <membuf.h>
#include <pager.h>
#include <linux/list.h>
#include <linux/build_bug.h>
#include <asm-offsets.h>
@@ -477,6 +478,14 @@ struct global_data {
*/
struct upl *upl;
#endif
#if CONFIG_IS_ENABLED(CONSOLE_PAGER)
/**
* @pager: Pointer to the pager settings, or NULL if none
*
* This is set up in console_init_r()
*/
struct pager *pager;
#endif
};
#ifndef DO_DEPS_ONLY
static_assert(sizeof(struct global_data) == GD_SIZE);
@@ -618,6 +627,16 @@ static_assert(sizeof(struct global_data) == GD_SIZE);
#define gd_passage_dtb() 0
#endif
#if CONFIG_IS_ENABLED(CONSOLE_PAGER)
#define gd_pager() gd->pager
#define gd_pagerp() &gd->pager
#define gd_pager_page_len() gd->pager->page_len
#else
#define gd_pager() NULL
#define gd_pagerp() NULL
#define gd_pager_page_len() 0
#endif
/**
* enum gd_flags - global data flags
*

View File

@@ -202,6 +202,16 @@ int console_clear(void);
*/
int console_remove_by_name(const char *name);
/**
* calc_check_console_lines() - Calculate console page length
*
* This calculates the appropriate number of lines to use for console paging,
* considering environment variables, config defaults, and device capabilities.
*
* Return: number of lines for paging, or 0 to disable paging
*/
int calc_check_console_lines(void);
/*
* CONSOLE multiplexing.
*/

View File

@@ -75,6 +75,12 @@
#define DFU_CALLBACK
#endif
#ifdef CONFIG_CONSOLE_PAGER
#define PAGER_CALLBACK "pager:pager,"
#else
#define PAGER_CALLBACK
#endif
/*
* This list of callback bindings is static, but may be overridden by defining
* a new association in the ".callbacks" environment variable.
@@ -88,6 +94,7 @@
DFU_CALLBACK \
"loadaddr:loadaddr," \
SILENT_CALLBACK \
PAGER_CALLBACK \
"stdin:console,stdout:console,stderr:console," \
"serial#:serialno," \
CFG_ENV_CALLBACK_LIST_STATIC

212
include/pager.h Normal file
View File

@@ -0,0 +1,212 @@
/* SPDX-License-Identifier: GPL-2.0+ */
/*
* Deals with splitting up text output into separate screenfuls
*
* Copyright 2025 Simon Glass <sjg@chromium.org>
*/
#ifndef __PAGER_H
#define __PAGER_H
#include <stdbool.h>
#include <abuf.h>
#include <membuf.h>
#include <linux/sizes.h>
#define PAGER_BUF_SIZE SZ_4K
/* Special return value from pager_next() indicating waiting for user input */
#define PAGER_WAITING ((const char *)1)
/* Prompt shown to user when pager reaches page limit */
#define PAGER_PROMPT "\n: Press SPACE to continue"
/* String used to blank/clear the pager prompt */
#define PAGER_BLANK "\r \r"
/**
* enum pager_state: Tracks the state of the pager
*
* @PAGERST_OK: Normal output is happening
* @PAGERST_AT_LIMIT: No more output can be provided; the next call to
* pager_next() will return a user prompt
* @PAGERST_WAIT_USER: Waiting for the user to press a key
* @PAGERST_CLEAR_PROMPT: Clearing the prompt ready for more output
* @PAGERST_BYPASS: Pager is being bypassed
*/
enum pager_state {
PAGERST_OK,
PAGERST_AT_LIMIT,
PAGERST_WAIT_USER,
PAGERST_CLEAR_PROMPT,
PAGERST_BYPASS,
};
/**
* struct pager - pager state
*
* The pager uses a buffer @buf to hold text that it is in the process of
* sending out. This helps deal with the stdio puts() interface, which does not
* permit passing a string length, only a string, which means that strings must
* be nul-terminated. The termination is handled automatically by the pager.
*
* If the text passed to pager_post() is too large for @buf then all the next
* will be written at once, without any paging, in the next call to
* pager_next().
*
* The membuf @mb is only used to feed out text in chunks, with a pager message
* (and a keypress wait) inserted between each chunk.
*
* @line_count: Number of lines output since last pause
* @page_len: Sets the height of the page in lines. The maximum lines to display
* before pausing is one less than this. Set from 'pager' env variable
* @buf: Buffer containing text to eventually be returned
* @mb: Circular buffer to manage @buf
* @overflow: pointer to overflow text to send nexts
* @nulch: pointer to where a nul character was written, NULL if none
* @oldch: old character that was at @nulch
* @state: current state of the pager state-machine
* @test_bypass: true if pager should behave as if in test mode (bypass all)
*/
struct pager {
int line_count;
int page_len;
struct abuf buf;
struct membuf mb;
const char *overflow;
char *nulch;
int oldch;
enum pager_state state;
bool test_bypass;
};
#if CONFIG_IS_ENABLED(CONSOLE_PAGER)
/**
* pager_post() - Add text to the input buffer for later handling
*
* If @use_pager the text is added to the pager buffer and fed out a screenful
* at a time. This function calls pager_post() after storing the text.
*
* After calling pager_post(), if it returns anything other than NULL, you must
* repeatedly call pager_next() until it returns NULL, otherwise text may be
* lost
*
* If @pag is NULL, this does nothing but return @s
*
* @pag: Pager to use, may be NULL
* @use_pager: Whether or not to use the pager functionality
* @s: Text to add
* Return: text which should be sent to output, or NULL if there is no more.
* If !@use_pager this just returns @s and does not affect the pager state
*/
const char *pager_post(struct pager *pag, bool use_pager, const char *s);
/**
* pager_next() - Returns the next screenful of text to show
*
* If this function returns PAGER_WAITING then the caller must check for user
* input and pass in the keypress in the next call to pager_next(). It can
* busy-wait for a keypress, if desired, since pager_next() will only ever
* return PAGER_WAITING until @ch is non-zero.
*
* @pag: Pager to use
* @use_pager: Whether or not to use the pager functionality
* @ch: Key that the user has pressed, or 0 if none
*
* Return: text which should be sent to output, or PAGER_WAITING if waiting for
* the user to press a key, or NULL if there is no more text.
* If !@use_pager this just returns NULL and does not affect the pager state
*/
const char *pager_next(struct pager *pag, bool use_pager, int ch);
/**
* pager_set_bypass() - put the pager into bypass mode
*
* This is used when output is not connected to a terminal. Bypass mode stops
* the pager from doing anything to interrupt output.
*
* @pag: Pager to use, may be NULL in which case this function does nothing
* @bypass: true to put the pager in bypass mode, false for normal mode
* Return: old value of the bypass flag
*/
bool pager_set_bypass(struct pager *pag, bool bypass);
/**
* pager_set_test_bypass() - put the pager into test bypass mode
*
* This is used for tests. Test bypass mode stops the pager from doing
* anything to interrupt output, regardless of the current pager state.
*
* @pag: Pager to use, may be NULL in which case this function does nothing
* @bypass: true to put the pager in test bypass mode, false for normal
* Return: old value of the test bypass flag
*/
bool pager_set_test_bypass(struct pager *pag, bool bypass);
/**
* pager_reset() - reset the line count in the pager
*
* Sets line_count to zero so that the pager starts afresh with its counting.
*
* @pag: Pager to update
*/
void pager_reset(struct pager *pag);
/**
* pager_uninit() - Uninit the pager
*
* Frees all memory and also @pag
*
* @pag: Pager to uninit
*/
void pager_uninit(struct pager *pag);
#else
static inline const char *pager_post(struct pager *pag, bool use_pager,
const char *s)
{
return s;
}
static inline const char *pager_next(struct pager *pag, bool use_pager, int ch)
{
return NULL;
}
static inline bool pager_set_bypass(struct pager *pag, bool bypass)
{
return true;
}
static inline bool pager_set_test_bypass(struct pager *pag, bool bypass)
{
return true;
}
static inline void pager_reset(struct pager *pag)
{
}
#endif
/**
* pager_set_page_len() - Set the page length of a pager
*
* @pag: Pager to use
* @page_len: Page length to set
*/
void pager_set_page_len(struct pager *pag, int page_len);
/**
* pager_init() - Set up a new pager
*
* @pagp: Returns allocaed pager, on success
* @page_len: Number of lines per page
* @buf_size: Buffer size to use in bytes, this is the maximum amount of output
* that can be paged
* Return: 0 if OK, -ENOMEM if out of memory
*/
int pager_init(struct pager **pagp, int page_len, int buf_size);
#endif

View File

@@ -406,8 +406,8 @@ int serial_tstc(void);
* When using a serial console or the net console we can only devise the
* terminal size by querying the terminal using ECMA-48 control sequences.
*
* @rowsp: returns number of rows
* @colsp: returns number of columns
* @rowsp: returns number of rows on success
* @colsp: returns number of columns on success
* Returns: 0 on success, -NOENT if no terminal is present, -ETIMEDOUT if we
* checked for a terminal but didn't get a response in time, -EPROTO if the
* terminal did not respond as expected

View File

@@ -22,6 +22,7 @@ config EFI_APP
depends on X86 || ARM
select CHARSET
select EVENT
imply CONSOLE_PAGER
help
Build U-Boot as an application which can be started from EFI. This
is useful for examining a platform in the early stages of porting

View File

@@ -184,6 +184,9 @@ static int bdinfo_test_all(struct unit_test_state *uts)
if (IS_ENABLED(CONFIG_VIDEO))
ut_assertok(test_video_info(uts));
if (IS_ENABLED(CONFIG_CONSOLE_PAGER))
ut_assert_nextline("pager = %d", gd_pager_page_len());
/* The gd->multi_dtb_fit may not be available, hence, #if below. */
#if CONFIG_IS_ENABLED(MULTI_DTB_FIT)
ut_assertok(test_num_l(uts, "multi_dtb_fit", (ulong)gd->multi_dtb_fit));

View File

@@ -7,7 +7,9 @@ obj-$(CONFIG_$(PHASE_)CMDLINE) += bloblist.o
endif
endif
obj-$(CONFIG_CONSOLE_PAGER) += console.o
obj-$(CONFIG_CYCLIC) += cyclic.o
obj-$(CONFIG_EVENT_DYNAMIC) += event.o
obj-y += cread.o
obj-$(CONFIG_CONSOLE_PAGER) += pager.o
obj-$(CONFIG_$(PHASE_)CMDLINE) += print.o

183
test/common/console.c Normal file
View File

@@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2025 Simon Glass <sjg@chromium.org>
*
* Test for console functionality
*/
#include <console.h>
#include <env.h>
#include <malloc.h>
#include <serial.h>
#include <video.h>
#include <test/common.h>
#include <test/test.h>
#include <test/ut.h>
#include <asm/global_data.h>
#include <asm/state.h>
DECLARE_GLOBAL_DATA_PTR;
/* Test calc_check_console_lines() with environment variable override */
static int console_test_calc_lines_env_override(struct unit_test_state *uts)
{
int original_lines, lines;
char *orig_env;
/* Save original environment value */
orig_env = env_get("pager");
original_lines = calc_check_console_lines();
/* Test with hex environment variable */
ut_assertok(env_set("pager", "1a")); /* 26 in hex */
lines = calc_check_console_lines();
ut_asserteq(26, lines);
/* Test with decimal-looking hex value */
ut_assertok(env_set("pager", "20")); /* 32 in hex */
lines = calc_check_console_lines();
ut_asserteq(32, lines);
/* Test with zero (should disable pager) */
ut_assertok(env_set("pager", "0"));
lines = calc_check_console_lines();
ut_asserteq(0, lines);
/* Restore original environment */
ut_assertok(env_set("pager", orig_env));
/* Verify restoration */
lines = calc_check_console_lines();
ut_asserteq(original_lines, lines);
return 0;
}
COMMON_TEST(console_test_calc_lines_env_override, 0);
/* Test calc_check_console_lines() with invalid environment values */
static int console_test_calc_lines_env_invalid(struct unit_test_state *uts)
{
int original_lines, lines;
char *orig_env;
/* Save original environment value */
orig_env = env_get("pager");
original_lines = calc_check_console_lines();
/* Test with invalid hex value - should fall back to default */
ut_assertok(env_set("pager", "xyz"));
lines = calc_check_console_lines();
/* Should return default since 'pager' reads as 0 */
ut_asserteq(CONFIG_CONSOLE_PAGER_LINES, lines);
/* Test with empty string */
ut_assertok(env_set("pager", ""));
lines = calc_check_console_lines();
ut_asserteq(CONFIG_CONSOLE_PAGER_LINES, lines);
/* Restore original environment */
ut_assertok(env_set("pager", orig_env));
return 0;
}
COMMON_TEST(console_test_calc_lines_env_invalid, 0);
/* Test calc_check_console_lines() default behavior without environment */
static int console_test_calc_lines_default(struct unit_test_state *uts)
{
int lines;
char *orig_env;
/* Save and clear environment variable */
orig_env = env_get("pager");
ut_assertok(env_set("pager", NULL));
/* Get default behavior */
lines = calc_check_console_lines();
/*
* The function considers device detection (video/serial) which may
* override the CONFIG default. In sandbox, serial_is_tty() may return
* false during tests, causing the function to return 0. Just verify it
* returns a reasonable value.
*/
ut_assert(lines >= 0);
/* If CONFIG_CONSOLE_PAGER is not enabled, should definitely be 0 */
if (!IS_ENABLED(CONFIG_CONSOLE_PAGER)) {
ut_asserteq(0, lines);
}
/* Restore original environment */
ut_assertok(env_set("pager", orig_env));
return 0;
}
COMMON_TEST(console_test_calc_lines_default, 0);
/* Test calc_check_console_lines() precedence: env overrides everything */
static int console_test_calc_lines_precedence(struct unit_test_state *uts)
{
int lines;
char *orig_env;
/* Save original environment value */
orig_env = env_get("pager");
/* Set environment to a specific value */
ut_assertok(env_set("pager", "2a")); /* 42 in hex */
lines = calc_check_console_lines();
/*
* Environment should always take precedence regardless of video/serial
* state
*/
ut_asserteq(42, lines);
/* Test with zero environment value */
ut_assertok(env_set("pager", "0"));
lines = calc_check_console_lines();
ut_asserteq(0, lines);
/* Restore original environment */
ut_assertok(env_set("pager", orig_env));
return 0;
}
COMMON_TEST(console_test_calc_lines_precedence, 0);
/* Test calc_check_console_lines() with serial TTY detection */
static int console_test_calc_lines_serial_tty(struct unit_test_state *uts)
{
int lines;
char *orig_env;
bool orig_tty;
struct sandbox_state *state;
/* Save original state */
orig_env = env_get("pager");
state = state_get_current();
orig_tty = state->serial_is_tty;
/* Clear environment to test device detection */
ut_assertok(env_set("pager", NULL));
/* Test with serial TTY enabled */
state->serial_is_tty = true;
lines = calc_check_console_lines();
/* Should attempt to query serial size or use default */
ut_assert(lines >= 0);
/* Test with serial TTY disabled (not a terminal) */
state->serial_is_tty = false;
lines = calc_check_console_lines();
/* Should return 0 when not connected to TTY */
ut_asserteq(0, lines);
/* Restore original state */
state->serial_is_tty = orig_tty;
ut_assertok(env_set("pager", orig_env));
return 0;
}
COMMON_TEST(console_test_calc_lines_serial_tty, 0);

580
test/common/pager.c Normal file
View File

@@ -0,0 +1,580 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Copyright 2025 Simon Glass <sjg@chromium.org>
*
* Test for pager functionality
*/
#include <errno.h>
#include <malloc.h>
#include <pager.h>
#include <string.h>
#include <test/common.h>
#include <test/test.h>
#include <test/ut.h>
#include <asm/global_data.h>
DECLARE_GLOBAL_DATA_PTR;
/* Test basic pager init and cleanup */
static int pager_test_basic_init(struct unit_test_state *uts)
{
struct pager *pag;
/* Test successful init */
ut_assertok(pager_init(&pag, 20, 1024));
ut_assertnonnull(pag);
ut_asserteq(20, pag->page_len);
ut_asserteq(0, pag->line_count);
ut_assertnull(pag->overflow);
ut_assertnull(pag->nulch);
/* Clean up */
pager_uninit(pag);
/* Test init with different parameters */
ut_assertok(pager_init(&pag, 10, 2048));
ut_assertnonnull(pag);
ut_asserteq(10, pag->page_len);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_basic_init, 0);
/* Test pager with simple text */
static int pager_test_simple_text(struct unit_test_state *uts)
{
struct pager *pag;
const char *text = "Hello, World!";
const char *result;
ut_assertok(pager_init(&pag, 20, 1024));
/* Post some text and get it back */
result = pager_post(pag, true, text);
ut_assertnonnull(result);
ut_asserteq_str(text, result);
/* Should be no more text */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_simple_text, 0);
/* Test pager with multiple lines */
static int pager_test_multiline(struct unit_test_state *uts)
{
struct pager *pag;
const char *text1 = "Line 1\n";
const char *text2 = "Line 2\n";
const char *text3 = "Line 3\n";
const char *result;
ulong start;
start = ut_check_free();
ut_assertok(pager_init(&pag, 20, 1024));
/* Post multiple pieces of text */
result = pager_post(pag, true, text1);
ut_assertnonnull(result);
ut_asserteq_str(text1, result);
/* Should be no more text after first post */
result = pager_next(pag, true, 0);
ut_assertnull(result);
result = pager_post(pag, true, text2);
ut_assertnonnull(result);
ut_asserteq_str(text2, result);
/* Should be no more text after second post */
result = pager_next(pag, true, 0);
ut_assertnull(result);
result = pager_post(pag, true, text3);
ut_assertnonnull(result);
ut_asserteq_str(text3, result);
/* Should be no more text after third post */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
/* Check for memory leaks */
ut_assertok(ut_check_delta(start));
return 0;
}
COMMON_TEST(pager_test_multiline, 0);
/* Test pager with large text that fills the buffer */
static int pager_test_large_text(struct unit_test_state *uts)
{
struct pager *pag;
const char *result;
ut_assertok(pager_init(&pag, 20, 16)); /* Small buffer */
/* Post large text - should fit in buffer */
result = pager_post(pag, true, "this is 16 chars");
ut_assertnonnull(result);
ut_asserteq_str("this is 16 chars", result);
ut_assertnull(pager_next(pag, true, 0));
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_large_text, 0);
/* Test pager overflow handling */
static int pager_test_overflow(struct unit_test_state *uts)
{
struct pager *pag;
const char *result;
ut_assertok(pager_init(&pag, 20, 4)); /* Small buffer */
/* send some text which is too long for the buffer */
result = pager_post(pag, true, "test1");
ut_assertnonnull(result);
/* overflow handling should return the text */
ut_asserteq_str("test1", result);
ut_assertnull(pager_next(pag, true, 0));
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_overflow, 0);
/* Test pager with NULL input */
static int pager_test_null_input(struct unit_test_state *uts)
{
const char *result;
/* Test pager_post with NULL pager */
result = pager_post(NULL, true, "test");
ut_asserteq_str("test", result);
return 0;
}
COMMON_TEST(pager_test_null_input, 0);
/* Test pager with empty strings */
static int pager_test_empty_strings(struct unit_test_state *uts)
{
struct pager *pag;
const char *result;
ut_assertok(pager_init(&pag, 20, 1024));
/* Post empty string */
result = pager_post(pag, true, "");
ut_assertnull(result);
/* Should be no more text */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_empty_strings, 0);
/* Test pager buffer management */
static int pager_test_buffer_management(struct unit_test_state *uts)
{
struct pager *pag;
const char *text = "Test buffer management";
const char *result;
ut_assertok(pager_init(&pag, 20, 1024));
/* Verify buffer is properly inited */
ut_assertnonnull(pag->buf.data);
ut_asserteq(1024, pag->buf.size);
/* Post text and verify buffer state */
result = pager_post(pag, true, text);
ut_assertnonnull(result);
/* Verify the buffer contains our text */
ut_asserteq_str(text, result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_buffer_management, 0);
/* Test pager with very long single line */
static int pager_test_long_single_line(struct unit_test_state *uts)
{
struct pager *pag;
char long_line[1000];
const char *result;
ut_assertok(pager_init(&pag, 20, 1024));
/* Create a very long line without newlines */
memset(long_line, 'X', sizeof(long_line) - 1);
long_line[sizeof(long_line) - 1] = '\0';
/* Post the long line */
result = pager_post(pag, true, long_line);
ut_assertnonnull(result);
/* Should get our text back */
ut_asserteq_str(long_line, result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_long_single_line, 0);
/* Test pager line counting and page breaks */
static int pager_test_line_counting(struct unit_test_state *uts)
{
struct pager *pag;
const char *multiline_text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n";
const char *result;
/* Init with page length of 4 lines */
ut_assertok(pager_init(&pag, 4, 1024));
/* Post multiline text */
result = pager_post(pag, true, multiline_text);
ut_assertnonnull(result);
/* Should get first 3 lines (excluding the 3rd newline) */
ut_asserteq_str("Line 1\nLine 2\nLine 3", result);
/* line_count is reset to 0 when page limit is reached */
ut_asserteq(0, pag->line_count);
/* Next call should return pager prompt */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str(PAGER_PROMPT, result);
/* Press space to continue */
result = pager_next(pag, true, ' ');
ut_assertnonnull(result);
ut_asserteq_str(PAGER_BLANK, result);
/* Get remaining lines */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str("Line 4\nLine 5\n", result);
/* Should be no more text */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_line_counting, 0);
/* Test that PAGER_WAITING is returned when pager waits for user input */
static int pager_test_pager_waiting(struct unit_test_state *uts)
{
struct pager *pag;
const char *result;
/* Create pager with small page size to trigger waiting quickly */
ut_assertok(pager_init(&pag, 3, 1024));
/* Post text that fills exactly the page limit */
result = pager_post(pag, true, "Line 1\nLine 2\n");
ut_assertnonnull(result);
ut_asserteq_str("Line 1\nLine 2", result);
/* Next call should return the prompt */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str(PAGER_PROMPT, result);
/* Next call without space key should return PAGER_WAITING */
result = pager_next(pag, true, 0);
ut_asserteq_ptr(PAGER_WAITING, result);
/* Another call without space should still return PAGER_WAITING */
result = pager_next(pag, true, 'x'); /* Wrong key */
ut_asserteq_ptr(PAGER_WAITING, result);
/* Pressing space should clear the prompt */
result = pager_next(pag, true, ' ');
ut_assertnonnull(result);
ut_asserteq_str(PAGER_BLANK, result);
/* Now should return NULL (no more content) */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_pager_waiting, 0);
/* Test use_pager parameter - output text directly, while buffer is non-empty */
static int pager_test_use_pager_param(struct unit_test_state *uts)
{
struct pager *pag;
const char *buffered_text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n";
const char *direct_text = "This should be written immediately";
const char *result;
/* Init with small page length to ensure paging occurs */
ut_assertok(pager_init(&pag, 3, 1024));
/* Post text with use_pager=true - should trigger paging */
result = pager_post(pag, true, buffered_text);
ut_assertnonnull(result);
/* Should get first 2 lines */
ut_asserteq_str("Line 1\nLine 2", result);
/* Now call pager_post with use_pager=false while text is still buffered */
result = pager_post(pag, false, direct_text);
/* Should get the text immediately, not from buffer */
ut_asserteq_ptr(direct_text, result);
/* Call pager_next with use_pager=false - should return NULL */
result = pager_next(pag, false, 0);
ut_assertnull(result);
/* Now continue with use_pager=true to get buffered text */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
/* Should get the pager prompt */
ut_asserteq_str(PAGER_PROMPT, result);
/* Press space to continue */
result = pager_next(pag, true, ' ');
ut_assertnonnull(result);
ut_asserteq_str(PAGER_BLANK, result);
/* Get remaining buffered lines - should be next 2 lines due to page limit */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str("Line 3\nLine 4", result);
/* Should get pager prompt again */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str(PAGER_PROMPT, result);
/* Press space to continue */
result = pager_next(pag, true, ' ');
ut_assertnonnull(result);
ut_asserteq_str(PAGER_BLANK, result);
/* Get final line */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str("Line 5\n", result);
/* Should be no more text */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_use_pager_param, 0);
/* Test pager bypass mode */
static int pager_test_bypass_mode(struct unit_test_state *uts)
{
struct pager *pag;
const char *text = "This text should be returned directly";
const char *result;
/* Init with small page length to ensure paging would normally occur */
ut_assertok(pager_init(&pag, 2, 1024));
/* Enable bypass mode */
pager_set_test_bypass(pag, true);
/* Post text - should get original string back directly */
result = pager_post(pag, true, text);
ut_asserteq_ptr(text, result); /* Should be same pointer */
/* pager_next should return NULL in bypass mode */
result = pager_next(pag, true, 0);
ut_assertnull(result);
/* Disable bypass mode */
pager_set_test_bypass(pag, false);
/* Now pager should work normally */
result = pager_post(pag, true, text);
ut_assertnonnull(result);
/* In normal mode, result should be different from original text */
ut_assert(result != text);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_bypass_mode, 0);
/* Test that single character output via putc goes through pager */
static int pager_test_putc(struct unit_test_state *uts)
{
struct pager *pag;
const char *result;
/* Init pager */
ut_assertok(pager_init(&pag, 20, 1024));
pager_set_bypass(pag, true);
/*
* Test that individual characters can be posted via pager API
* This verifies that console_putc_pager() routes through the pager
* system
*/
result = pager_post(pag, true, "A");
ut_asserteq_ptr("A", result); /* Bypass mode returns original pointer */
result = pager_post(pag, true, "\n");
ut_asserteq_ptr("\n", result);
result = pager_post(pag, true, "B");
ut_asserteq_ptr("B", result);
/* Disable bypass to test normal functionality with single chars */
pager_set_bypass(pag, false);
result = pager_post(pag, true, "X");
ut_assertnonnull(result);
ut_asserteq_str("X", result);
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_putc, 0);
/* Test writing up to page limit then adding final newline */
static int pager_test_limit_plus_newline(struct unit_test_state *uts)
{
struct pager *pag;
const char *result;
/* Init with page length of 3 lines */
ut_assertok(pager_init(&pag, 3, 1024));
/* Write text that reaches exactly the page limit (2 newlines) */
result = pager_post(pag, true, "Line 1\nLine 2");
ut_assertnonnull(result);
ut_asserteq_str("Line 1\nLine 2", result);
ut_asserteq(1, pag->line_count); /* Should have 1 line counted */
/* Should be no more text yet - haven't hit limit */
result = pager_next(pag, true, 0);
ut_assertnull(result);
/* Now post a single newline - this should trigger the page limit */
result = pager_post(pag, true, "\n");
ut_assertnonnull(result);
/*
* Should get empty string since we hit the limit and the newline is
* consumed
*/
ut_asserteq_str("", result);
/* Next call should return the pager prompt since we hit the limit */
result = pager_next(pag, true, 0);
ut_assertnonnull(result);
ut_asserteq_str(PAGER_PROMPT, result);
/* Press space to continue */
result = pager_next(pag, true, ' ');
ut_assertnonnull(result);
ut_asserteq_str(PAGER_BLANK, result);
/* Should be no more text */
result = pager_next(pag, true, 0);
ut_assertnull(result);
pager_uninit(pag);
return 0;
}
COMMON_TEST(pager_test_limit_plus_newline, 0);
/* Test console integration - pager prompt appears in console output */
static int pager_test_console(struct unit_test_state *uts)
{
struct pager *pag, *orig_pag;
char line[100];
int avail, ret;
/* Save original pager */
orig_pag = gd_pager();
/* Create our own pager for testing */
ret = pager_init(&pag, 2, 1024);
if (ret) {
gd->pager = orig_pag;
return CMD_RET_FAILURE;
}
/* Set up pager to be one away from limit (1 line already counted) */
pag->line_count = 1;
/* Assign our pager to the global data */
gd->pager = pag;
/* Trigger paging with a second newline */
putc('\n');
/* Check if there's any console output available at all */
avail = console_record_avail();
/* Restore original pager first */
gd->pager = orig_pag;
pager_uninit(pag);
/* Now check what we got */
if (!avail) {
ut_reportf("No console output was recorded at all");
return CMD_RET_FAILURE;
}
/* Try to read the actual output */
ret = console_record_readline(line, sizeof(line));
if (ret < 0) {
ut_reportf("Failed to read first line, avail was %d", avail);
return CMD_RET_FAILURE;
}
/*
* console recording does not see the pager prompt, so we should have
* just got a newline
*/
ut_asserteq_str("", line);
ut_assert_console_end();
return 0;
}
COMMON_TEST(pager_test_console, UTF_CONSOLE);

View File

@@ -52,6 +52,13 @@ class ConsoleSandbox(ConsoleBase):
if self.use_dtb:
cmd += ['-d', self.config.dtb]
cmd += self.sandbox_flags
# Always disable the pager
cmd.append('-P')
# Always disable detected the terminal size
cmd.append('-A')
return Spawn(cmd, cwd=self.config.source_dir, decode_signal=True)
def restart_uboot_with_flags(self, flags, use_dtb=True):

View File

@@ -14,6 +14,7 @@
#include <net.h>
#include <of_live.h>
#include <os.h>
#include <pager.h>
#include <spl.h>
#include <usb.h>
#include <dm/ofnode.h>
@@ -747,8 +748,10 @@ int ut_run_list(struct unit_test_state *uts, const char *category,
memcpy(uts->fdt_copy, gd->fdt_blob, uts->fdt_size);
}
uts->force_run = force_run;
pager_set_test_bypass(gd_pager(), true);
ret = ut_run_tests(uts, prefix, tests, count, select_name,
test_insert);
pager_set_test_bypass(gd_pager(), false);
/* Best efforts only...ignore errors */
if (has_dm_tests)