mirror of
https://github.com/espressif/esp-idf.git
synced 2026-04-27 19:13:21 +00:00
fix(console): add linenoise multithreading support for Picolibc
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user