fix(console): add linenoise multithreading support for Picolibc

This commit is contained in:
Alexey Lapshin
2025-11-28 14:27:31 +07:00
parent 068fefad68
commit 535d93e149
10 changed files with 237 additions and 6 deletions
+11
View File
@@ -1,5 +1,8 @@
idf_build_get_property(target IDF_TARGET)
# Note: Almost all source files in this component include console_stdio_private.h.
# This header only affects LIBC_PICOLIBC builds, not LIBC_NEWLIB builds.
# It enables thread-local stdio streams, which are required for console functionality in some cases.
set(srcs "commands.c"
"esp_console_common.c"
"esp_console_repl_internal.c"
@@ -41,6 +44,14 @@ idf_component_register(SRCS ${srcs}
esp_usb_cdc_rom_console
)
if(CONFIG_LIBC_PICOLIBC)
list(APPEND srcs_include_stdio_private ${srcs})
list(APPEND srcs_include_stdio_private ${argtable_srcs})
list(REMOVE_ITEM srcs_include_stdio_private "esp_console_repl_chip.c" "esp_console_repl_linux.c")
set_source_files_properties(${srcs_include_stdio_private}
PROPERTIES COMPILE_FLAGS "--include=console_stdio_private.h")
endif()
if(CONFIG_COMPILER_STATIC_ANALYZER AND CMAKE_C_COMPILER_ID STREQUAL "GNU") # TODO IDF-10085
target_compile_options(${COMPONENT_LIB} PRIVATE "-fno-analyzer")
endif()
+28 -1
View File
@@ -30,6 +30,19 @@ esp_err_t esp_console_setup_prompt(const char *prompt, esp_console_repl_com_t *r
snprintf(repl_com->prompt, CONSOLE_PROMPT_MAX_LEN - 1, LOG_COLOR_I "%s " LOG_RESET_COLOR, prompt_temp);
/* Figure out if the terminal supports escape sequences */
/* TODO IDF-14901: It is not appropriate to probe the current thread's console here.
* The esp_console_repl_task can open its own stdin/stdout for use.
* However, linenoiseProbe() cannot be moved to esp_console_repl_task
* to preserve user expectations. Consider the following usage pattern:
* esp_console_start_repl(repl);
* printf("!!!ready!!!");
* Users expect that when "!!!ready!!!" is printed, the console is already available.
* If linenoiseProbe() were moved to esp_console_repl_task, race conditions
* between threads combined with usleep() calls inside linenoiseProbe() could
* change this behavior. Currently, there is already a race between threads,
* but since esp_console_repl_task does not call any sleep functions, everything
* works as users expect.
*/
int probe_status = linenoiseProbe();
if (probe_status) {
/* zero indicates success */
@@ -159,7 +172,6 @@ void esp_console_repl_task(void *args)
{
esp_console_repl_universal_t *repl_conf = (esp_console_repl_universal_t *) args;
esp_console_repl_com_t *repl_com = &repl_conf->repl_com;
const int uart_channel = repl_conf->uart_channel;
/* Waiting for task notify. This happens when `esp_console_start_repl()`
* function is called. */
@@ -172,6 +184,16 @@ void esp_console_repl_task(void *args)
/* Change standard input and output of the task if the requested UART is
* NOT the default one. This block will replace stdin, stdout and stderr.
*/
#if CONFIG_LIBC_PICOLIBC
// TODO IDF-14901
if (repl_com->_stdin) {
stdin = repl_com->_stdin;
stdout = stderr = repl_com->_stdout;
} else {
linenoise_init_with_global_stdio();
}
#else
const int uart_channel = repl_conf->uart_channel;
if (uart_channel != CONFIG_ESP_CONSOLE_UART_NUM) {
char path[CONSOLE_PATH_MAX_LEN] = { 0 };
snprintf(path, CONSOLE_PATH_MAX_LEN, "/dev/uart/%d", uart_channel);
@@ -180,6 +202,7 @@ void esp_console_repl_task(void *args)
stdout = fopen(path, "w");
stderr = stdout;
}
#endif
/* Disable buffering on stdin of the current task.
* If the console is ran on a different UART than the default one,
@@ -231,6 +254,10 @@ void esp_console_repl_task(void *args)
linenoiseFree(line);
}
#if CONFIG_LIBC_PICOLIBC
linenoise_close_stdio();
#endif
if (repl_com->state_mux != NULL) {
xSemaphoreGive(repl_com->state_mux);
}
@@ -252,9 +252,42 @@ esp_err_t esp_console_new_repl_uart(const esp_console_dev_uart_config_t *dev_con
goto _exit;
}
#if CONFIG_LIBC_PICOLIBC // TODO IDF-14901
#if !CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY
#define tls_stdin linenoise_stdin
#define tls_stdout linenoise_stdout
#endif
extern __thread FILE *tls_stdin;
extern __thread FILE *tls_stdout;
// Workaround for Picolibc to use thread-local stdio streams when the console is not the default one.
// Need to set linenoise_stdin/linenoise_stdout to correct values that will be used by the esp_console_repl_task
// before esp_console_setup_prompt() call, because it uses them. After that, we can restore the original values.
if (dev_config->channel != CONFIG_ESP_CONSOLE_UART_NUM) {
char path[CONSOLE_PATH_MAX_LEN] = { 0 };
snprintf(path, CONSOLE_PATH_MAX_LEN, "/dev/uart/%d", dev_config->channel);
uart_repl->repl_com._stdin = fopen(path, "r");
uart_repl->repl_com._stdout = fopen(path, "w");
}
FILE *tmp_stdin = stdin;
FILE *tmp_stdout = stdout;
if (uart_repl->repl_com._stdin) {
tls_stdin = uart_repl->repl_com._stdin;
tls_stdout = uart_repl->repl_com._stdout;
}
#endif
// setup prompt
esp_console_setup_prompt(repl_config->prompt, &uart_repl->repl_com);
#if CONFIG_LIBC_PICOLIBC // TODO IDF-14901
if (uart_repl->repl_com._stdin) {
// Restore the original values of tls_stdin and tls_stdout just in case.
tls_stdin = tmp_stdin;
tls_stdout = tmp_stdout;
}
#endif
/* Fill the structure here as it will be used directly by the created task. */
uart_repl->uart_channel = dev_config->channel;
uart_repl->repl_com.state = CONSOLE_REPL_STATE_INIT;
+8
View File
@@ -124,6 +124,14 @@
#include <sys/param.h>
#include <assert.h>
#include "linenoise.h"
#if CONFIG_LIBC_PICOLIBC
#include <stdio-bufio.h>
#endif
#if CONFIG_LIBC_PICOLIBC && !CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY
__thread FILE *linenoise_stdin;
__thread FILE *linenoise_stdout;
#endif
#define LINENOISE_DEFAULT_HISTORY_MAX_LEN 100
#define LINENOISE_DEFAULT_MAX_LINE 4096
@@ -31,6 +31,10 @@ typedef enum {
typedef struct {
esp_console_repl_t repl_core; // base class
#if CONFIG_LIBC_PICOLIBC
FILE *_stdin;
FILE *_stdout;
#endif
char prompt[CONSOLE_PROMPT_MAX_LEN]; // Prompt to be printed before each line
repl_state_t state;
SemaphoreHandle_t state_mux;
@@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* This file provides thread-local storage for stdin/stdout streams when using
* picolibc to have thread-local stdio streams instead of global ones.
* It enables per-thread stdio redirection by:
* - Defining thread-local FILE* variables (tls_stdin, tls_stdout)
* - Redefining standard I/O macros (stdin, stdout, printf, scanf, etc.) to use TLS streams
* - Providing initialization and cleanup functions for TLS stdio streams
*/
#pragma once
#include <stdio.h>
#include "sdkconfig.h"
#ifdef __cplusplus
extern "C" {
#endif
#if CONFIG_LIBC_PICOLIBC && !CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY
#define tls_stdin linenoise_stdin
#define tls_stdout linenoise_stdout
#endif
#if CONFIG_LIBC_PICOLIBC
extern __thread FILE *tls_stdin;
extern __thread FILE *tls_stdout;
#endif
static inline void linenoise_init_with_global_stdio(void)
{
#if CONFIG_LIBC_PICOLIBC
tls_stdin = stdin;
tls_stdout = stdout;
#endif
}
static inline void linenoise_close_stdio(void)
{
#if CONFIG_LIBC_PICOLIBC
if (tls_stdin != stdin) {
fclose(tls_stdin);
}
if (tls_stdout != stdout) {
fclose(tls_stdout);
}
#endif
}
#if CONFIG_LIBC_PICOLIBC
#undef stdin
#define stdin tls_stdin
#undef stdout
#define stdout tls_stdout
#define printf(...) fprintf(tls_stdout, __VA_ARGS__)
#define vprintf(fmt, ap) vfprintf(tls_stdout, fmt, ap)
#ifdef putchar
#undef putchar
#endif
#define putchar(c) fputc((c), tls_stdout)
#define puts(s) fputs((s), tls_stdout)
#define scanf(...) fscanf(tls_stdin, __VA_ARGS__)
#define vscanf(fmt, ap) vfscanf(tls_stdin, fmt, ap)
#ifdef getchar
#undef getchar
#endif
#define getchar() fgetc(tls_stdin)
#define gets(buf) fgets((buf), sizeof(buf), tls_stdin)
#endif
#ifdef __cplusplus
}
#endif
@@ -1,4 +1,10 @@
set(priv_requires unity console)
if(NOT CONFIG_IDF_TARGET_LINUX)
list(APPEND priv_requires esp_driver_uart)
endif()
idf_component_register(SRCS "test_app_main.c" "test_console.c"
INCLUDE_DIRS "."
PRIV_REQUIRES unity console
PRIV_REQUIRES ${priv_requires}
WHOLE_ARCHIVE)
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
@@ -8,6 +8,9 @@
#include "unity_test_runner.h"
#include "unity_test_utils_memory.h"
#include <sys/time.h>
#if !CONFIG_IDF_TARGET_LINUX
#include "driver/uart.h"
#endif
// Some resources are lazy allocated (newlib locks) in the console code, the threshold is left for that case
#define TEST_MEMORY_LEAK_THRESHOLD_DEFAULT (150)
@@ -36,6 +39,15 @@ void app_main(void)
struct timeval tv = { 0 };
gettimeofday(&tv, NULL);
#if !CONFIG_IDF_TARGET_LINUX
/* Preallocate UART1 memory */
fileno(stdin);
uart_driver_install(UART_NUM_1, 256, 0, 0, NULL, 0);
FILE *f = fopen("/dev/uart/1", "rw");
fclose(f);
uart_driver_delete(UART_NUM_1);
#endif
printf("Running console component tests\n");
unity_run_menu();
}
@@ -13,6 +13,9 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#if !CONFIG_IDF_TARGET_LINUX
#include "driver/uart.h"
#endif
/*
* NOTE: Most of these unit tests DO NOT work standalone. They require pytest to control
@@ -152,7 +155,7 @@ static esp_console_cmd_t s_quit_cmd = {
ran separately in test_console_repl */
TEST_CASE("esp console repl test", "[console][ignore]")
{
set_leak_threshold(416);
set_leak_threshold(248);
s_test_console_mutex = xSemaphoreCreateMutexStatic(&s_test_console_mutex_buf);
TEST_ASSERT_NOT_NULL(s_test_console_mutex);
@@ -189,7 +192,7 @@ TEST_CASE("esp console repl test", "[console][ignore]")
TEST_CASE("esp console repl deinit", "[console][ignore]")
{
set_leak_threshold(416);
set_leak_threshold(248);
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
@@ -404,3 +407,33 @@ TEST_CASE("esp console re-register commands", "[console][ignore]")
TEST_ESP_OK(esp_console_start_repl(s_repl));
vTaskDelay(pdMS_TO_TICKS(5000));
}
#if !CONFIG_IDF_TARGET_LINUX
TEST_CASE("esp console repl custom_uart test", "[console][ignore]")
{
set_leak_threshold(248);
printf("Running repl on UART1\n");
esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
uart_config.channel = UART_NUM_1; // Set UART1 for repl task
TEST_ESP_OK(esp_console_new_repl_uart(&uart_config, &repl_config, &s_repl));
TEST_ESP_OK(esp_console_cmd_register(&s_quit_cmd));
TEST_ESP_OK(esp_console_start_repl(s_repl));
/* Wait a little for repl console initialization on UART1 */
vTaskDelay(pdMS_TO_TICKS(300));
TEST_ESP_OK(esp_console_stop_repl(s_repl));
/* Let scheduler clean task internals */
vTaskDelay(pdMS_TO_TICKS(50));
printf("ByeBye\r\n");
}
#endif // !CONFIG_IDF_TARGET_LINUX
@@ -19,7 +19,7 @@ def do_test_repl_deinit(dut: Dut) -> None:
def do_test_help_generic(dut: Dut, registration_order: str) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console help command - {} registration"'.format(registration_order), expect_str='esp>')
dut.confirm_write(f'"esp console help command - {registration_order} registration"', expect_str='esp>')
dut.confirm_write('help', expect_str='aaa')
@@ -269,3 +269,21 @@ def test_console_help_re_register(dut: Dut, test_on: str) -> None:
dut.expect_exact('should appear last in help')
dut.expect_exact('should appear first in help')
@idf_parametrize('config', ['defaults'], indirect=['config'])
@idf_parametrize(
'target,test_on,markers',
[
('esp32', 'target', (pytest.mark.generic,)),
('esp32c3', 'target', (pytest.mark.generic,)),
('esp32', 'qemu', (pytest.mark.host_test, pytest.mark.qemu)),
],
indirect=['target'],
)
def test_console_custom_uart_repl(dut: Dut, test_on: str) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.confirm_write('"esp console repl custom_uart test"', expect_str='Running repl on UART1')
# make sure that global stdout has not been changed
dut.expect_exact('ByeBye')