From 52004409ceebb64bf0de84ec9808d8e6c0af46e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Wed, 1 Apr 2026 15:27:00 +0200 Subject: [PATCH] fix(esp_stdio): remap console select fd bits to sink device Map logical /dev/console fds to the primary sink fd for select. This avoids probing UART1/UART2 for stdout/stderr and keeps UART select state consistent. Add stdio regression checks for logical fd mapping and symmetric uart_end_select teardown. --- components/esp_driver_uart/src/uart_vfs.c | 1 - components/esp_stdio/stdio_vfs.c | 158 +++++++++++++++++- .../test_apps/stdio/main/test_app_main.c | 35 ++++ .../test_apps/stdio/pytest_esp_stdio_tests.py | 6 + 4 files changed, 192 insertions(+), 8 deletions(-) diff --git a/components/esp_driver_uart/src/uart_vfs.c b/components/esp_driver_uart/src/uart_vfs.c index fa9221f2a58..69fef254ed5 100644 --- a/components/esp_driver_uart/src/uart_vfs.c +++ b/components/esp_driver_uart/src/uart_vfs.c @@ -588,7 +588,6 @@ static esp_err_t uart_end_select(void *end_select_args) if (s_uart_select_count[i] == 0) { uart_set_select_notif_callback(i, NULL); } - break; } } portEXIT_CRITICAL(uart_get_selectlock()); diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index bf0351fde23..2b2dab96140 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -35,6 +35,8 @@ #include "esp_private/startup_internal.h" #include "esp_private/nullfs.h" +#include "esp_heap_caps.h" +#endif #define STRINGIFY(s) STRINGIFY2(s) #define STRINGIFY2(s) #s @@ -322,29 +324,171 @@ int console_access(__attribute__((unused)) void *ctx, const char *path, int amod #endif // CONFIG_VFS_SUPPORT_DIR #ifdef CONFIG_VFS_SUPPORT_SELECT + +/* + * Logical /dev/console fds (0,1,2,...) are not UART port numbers. uart_vfs select uses fd_set bits as + * SOC UART indices; without remapping, monitoring stdout/stderr selects UART1/UART2 and corrupts + * s_uart_select_count / ISR registration (only UART0 is the console sink). + */ +typedef struct { + void *uart_args; + fd_set *readfds; + fd_set *writefds; + fd_set *exceptfds; + uint64_t interested_read; + uint64_t interested_write; + uint64_t interested_except; +} console_select_ctx_t; + +/* Kconfig allows up to 64 logical fds; uint64_t masks and (1ULL << i) require i < 64. */ +enum { CONSOLE_SELECT_MAX_SCAN = 64 }; + static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, esp_vfs_select_sem_t select_sem, void **end_select_args) { - // start_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig const mux_entry_t *entry = get_primary_entry(); assert(entry); - if (entry->fd >= 0 && entry->ops->select && entry->ops->select->start_select) { - return entry->ops->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); + *end_select_args = NULL; + + if (entry->fd < 0 || entry->ops->select == NULL || entry->ops->select->start_select == NULL) { + return ESP_ERR_NOT_SUPPORTED; } - return ESP_ERR_NOT_SUPPORTED; + uint64_t interested_read = 0; + uint64_t interested_write = 0; + uint64_t interested_except = 0; + + const int scan = nfds < CONSOLE_SELECT_MAX_SCAN ? nfds : CONSOLE_SELECT_MAX_SCAN; + for (int i = 0; i < scan; ++i) { + if (!is_fd_valid(i)) { + continue; + } + if (readfds && FD_ISSET(i, readfds)) { + interested_read |= (1ULL << i); + } + if (writefds && FD_ISSET(i, writefds)) { + interested_write |= (1ULL << i); + } + if (exceptfds && FD_ISSET(i, exceptfds)) { + interested_except |= (1ULL << i); + } + } + + if (interested_read == 0 && interested_write == 0 && interested_except == 0) { + return ESP_ERR_NOT_SUPPORTED; + } + + const int hw = entry->fd; + + if (readfds) { + FD_ZERO(readfds); + if (interested_read) { + FD_SET(hw, readfds); + } + } + if (writefds) { + FD_ZERO(writefds); + if (interested_write) { + FD_SET(hw, writefds); + } + } + if (exceptfds) { + FD_ZERO(exceptfds); + if (interested_except) { + FD_SET(hw, exceptfds); + } + } + + const int forward_nfds = nfds > hw + 1 ? nfds : hw + 1; + + console_select_ctx_t *ctx = heap_caps_malloc(sizeof(console_select_ctx_t), MALLOC_CAP_INTERNAL); + if (ctx == NULL) { + return ESP_ERR_NO_MEM; + } + + ctx->uart_args = NULL; + ctx->readfds = readfds; + ctx->writefds = writefds; + ctx->exceptfds = exceptfds; + ctx->interested_read = interested_read; + ctx->interested_write = interested_write; + ctx->interested_except = interested_except; + + esp_err_t err = entry->ops->select->start_select(forward_nfds, readfds, writefds, exceptfds, select_sem, + &ctx->uart_args); + if (err != ESP_OK) { + if (readfds) { + FD_ZERO(readfds); + for (int i = 0; i < scan; ++i) { + if (interested_read & (1ULL << i)) { + FD_SET(i, readfds); + } + } + } + if (writefds) { + FD_ZERO(writefds); + for (int i = 0; i < scan; ++i) { + if (interested_write & (1ULL << i)) { + FD_SET(i, writefds); + } + } + } + if (exceptfds) { + FD_ZERO(exceptfds); + for (int i = 0; i < scan; ++i) { + if (interested_except & (1ULL << i)) { + FD_SET(i, exceptfds); + } + } + } + heap_caps_free(ctx); + return err; + } + + *end_select_args = ctx; + return ESP_OK; +} + +static void console_expand_fdset(fd_set *fds, uint64_t interested, int hw) +{ + if (fds == NULL) { + return; + } + const bool ready = FD_ISSET(hw, fds); + FD_ZERO(fds); + if (!ready || interested == 0) { + return; + } + for (int i = 0; i < CONSOLE_SELECT_MAX_SCAN; ++i) { + if (interested & (1ULL << i)) { + FD_SET(i, fds); + } + } } esp_err_t console_end_select(void *end_select_args) { - // end_select is not guaranteed be implemented even though CONFIG_VFS_SUPPORT_SELECT is enabled in sdkconfig + if (end_select_args == NULL) { + return ESP_ERR_INVALID_ARG; + } + console_select_ctx_t *ctx = (console_select_ctx_t *)end_select_args; const mux_entry_t *entry = get_primary_entry(); assert(entry); + + esp_err_t ret = ESP_OK; if (entry->fd >= 0 && entry->ops->select && entry->ops->select->end_select) { - return entry->ops->select->end_select(end_select_args); + ret = entry->ops->select->end_select(ctx->uart_args); } - return ESP_ERR_NOT_SUPPORTED; + const int hw = entry->fd; + if (hw >= 0) { + console_expand_fdset(ctx->readfds, ctx->interested_read, hw); + console_expand_fdset(ctx->writefds, ctx->interested_write, hw); + console_expand_fdset(ctx->exceptfds, ctx->interested_except, hw); + } + + heap_caps_free(ctx); + return ret; } #endif // CONFIG_VFS_SUPPORT_SELECT diff --git a/components/esp_stdio/test_apps/stdio/main/test_app_main.c b/components/esp_stdio/test_apps/stdio/main/test_app_main.c index 42548f27566..154b83814b4 100644 --- a/components/esp_stdio/test_apps/stdio/main/test_app_main.c +++ b/components/esp_stdio/test_apps/stdio/main/test_app_main.c @@ -19,6 +19,7 @@ #include #include +#include #if CONFIG_ESP_CONSOLE_NONE /* Set up UART on UART_0 (console) to be able to @@ -171,6 +172,37 @@ static void stdio_fd_init_check(void) fprintf(stderr, "STDIO_TEST:STREAM:STDERR\n"); fflush(stderr); } + +#if CONFIG_VFS_SUPPORT_SELECT && CONFIG_ESP_CONSOLE_UART +static void stdio_select_fd_mapping_check(void) +{ + bool driver_installed_here = false; + esp_err_t err = uart_driver_install(CONFIG_ESP_CONSOLE_UART_NUM, 256, 0, 0, NULL, 0); + if (err == ESP_OK) { + driver_installed_here = true; + } else { + assert(err == ESP_ERR_INVALID_STATE); + } + + fd_set writefds; + FD_ZERO(&writefds); + FD_SET(fileno(stdout), &writefds); + FD_SET(fileno(stderr), &writefds); + + struct timeval tv = { + .tv_sec = 0, + .tv_usec = 0, + }; + + errno = 0; + int ret = select(fileno(stderr) + 1, NULL, &writefds, NULL, &tv); + assert(ret >= 0); + if (driver_installed_here) { + assert(uart_driver_delete(CONFIG_ESP_CONSOLE_UART_NUM) == ESP_OK); + } + printf("STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK\n"); +} +#endif #endif // CONFIG_VFS_SUPPORT_IO void app_main(void) @@ -188,6 +220,9 @@ void app_main(void) close(fd); stdio_fd_init_check(); +#if CONFIG_VFS_SUPPORT_SELECT && CONFIG_ESP_CONSOLE_UART + stdio_select_fd_mapping_check(); +#endif #endif // CONFIG_VFS_SUPPORT_IO #if CONFIG_ESP_CONSOLE_NONE diff --git a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py index a545264bfb4..cd3ed8ef73e 100644 --- a/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py +++ b/components/esp_stdio/test_apps/stdio/pytest_esp_stdio_tests.py @@ -92,6 +92,7 @@ def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None: dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') dut.expect('STDIO_TEST:STREAM:STDOUT') dut.expect('STDIO_TEST:STREAM:STDERR') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('Opening /dev/console') dut.expect('This should be printed to stdout') dut.expect('Closing /dev/console') @@ -113,6 +114,7 @@ def test_esp_system_stdio_correct_open_and_close(dut: Dut) -> None: def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None: dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:UNIQUE_OK') dut.expect('STDIO_TEST:NON_BASIC:REUSE_OK') @@ -130,6 +132,7 @@ def test_esp_stdio_non_basic_default_qemu(dut: Dut) -> None: def test_esp_stdio_non_basic_small_fd_mode_qemu(dut: Dut) -> None: dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK') @@ -144,6 +147,7 @@ def test_esp_stdio_non_basic_small_fd_mode_qemu(dut: Dut) -> None: def test_esp_stdio_basic_mode_qemu(dut: Dut) -> None: dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=BASIC') dut.expect('STDIO_TEST:BASIC:OPEN_OK') dut.expect('STDIO_TEST:BASIC:WRITE_OK') @@ -160,6 +164,7 @@ def test_esp_stdio_non_basic_small_fd_mode(dut: Dut) -> None: _expect_app_main_banner(dut) dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=NON_BASIC') dut.expect('STDIO_TEST:NON_BASIC:SATURATED_OK') @@ -173,6 +178,7 @@ def test_esp_stdio_basic_mode(dut: Dut) -> None: _expect_app_main_banner(dut) dut.expect('Hello World') dut.expect('STDIO_TEST:FD_INIT:IN=0,OUT=1,ERR=2') + dut.expect('STDIO_TEST:SELECT:LOGICAL_FD_MAP_OK') dut.expect('STDIO_TEST:MODE=BASIC') dut.expect('STDIO_TEST:BASIC:OPEN_OK') dut.expect('STDIO_TEST:BASIC:WRITE_OK')