From e08bed82c67c146a7ac85bc42b9c8056a2387d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Thu, 18 Dec 2025 00:13:42 +0100 Subject: [PATCH] feat(storage/stdio): Refactor stdio_vfs multiplexer --- components/esp_stdio/CMakeLists.txt | 6 +- components/esp_stdio/Kconfig | 21 ++ components/esp_stdio/stdio_vfs.c | 421 ++++++++++++++++++++++------ 3 files changed, 366 insertions(+), 82 deletions(-) diff --git a/components/esp_stdio/CMakeLists.txt b/components/esp_stdio/CMakeLists.txt index b929cc4e4a7..c1ec2a0c87b 100644 --- a/components/esp_stdio/CMakeLists.txt +++ b/components/esp_stdio/CMakeLists.txt @@ -7,7 +7,6 @@ if(non_os_build) idf_component_register() return() endif() - set(srcs) set(includes) @@ -18,10 +17,13 @@ if(${target} STREQUAL "linux") "linux/include") else() list(APPEND srcs "stdio_port.c" - "stdio_vfs.c" "stdio_simple.c" "stdio_syscalls_simple.c") list(APPEND includes "include") + + if(CONFIG_VFS_SUPPORT_IO) + list(APPEND srcs "stdio_vfs.c") + endif() endif() idf_component_register(SRCS ${srcs} diff --git a/components/esp_stdio/Kconfig b/components/esp_stdio/Kconfig index a103ba0c526..996ba13ff34 100644 --- a/components/esp_stdio/Kconfig +++ b/components/esp_stdio/Kconfig @@ -185,4 +185,25 @@ menu "ESP-STDIO" If enabled, esp_rom_printf and ESP_EARLY_LOG output will also be sent over USB CDC. Disabling this option saves about 1kB or RAM. + config ESP_STDIO_MAX_FDS + int "Max logical stdio file descriptors" + depends on VFS_SUPPORT_IO + range 0 64 + default 3 + help + Maximum number of logical /dev/console descriptors tracked by esp_stdio VFS. + - 0 enables basic mode with a shared internal descriptor. + - 1..2 is not allowed + - Values >3 enable descriptor-table mode. + + Enabling basic mode decreases RAM usage, but disables checks for per-fd behaviour, such as read-only. + + config ESP_STDIO_MAX_VFS_ENTRIES + int "Maximum number of registerable VFS sinks" + depends on VFS_SUPPORT_IO + range 1 10 + default 2 + help + Maximum number of filesystems that can be registered as sinks for console output. + endmenu diff --git a/components/esp_stdio/stdio_vfs.c b/components/esp_stdio/stdio_vfs.c index 28fd6487582..06d96e1dbee 100644 --- a/components/esp_stdio/stdio_vfs.c +++ b/components/esp_stdio/stdio_vfs.c @@ -5,10 +5,15 @@ */ #include "sdkconfig.h" +#include +#include #include +#include #include "esp_err.h" +#include "esp_log.h" #include "esp_rom_sys.h" #include "esp_stdio.h" +#include "esp_vfs.h" #include #if CONFIG_VFS_SUPPORT_IO @@ -42,112 +47,277 @@ * while the secondary is only used for output. */ +#if CONFIG_VFS_SUPPORT_IO + +_Static_assert(CONFIG_ESP_STDIO_MAX_FDS == 0 || CONFIG_ESP_STDIO_MAX_FDS >= 3, "Invalid value for CONFIG_ESP_STDIO_MAX_FDS"); + +#define ESP_STDIO_IS_BASIC (CONFIG_ESP_STDIO_MAX_FDS <= 0) + typedef struct { - int fd_primary; - int fd_secondary; -} vfs_console_context_t; + const esp_vfs_fs_ops_t *ops; + int vfs_flags; + void *vfs_ctx; + const char *path; + int fd; +} mux_entry_t; -// Secondary register part. -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG -const static esp_vfs_fs_ops_t *secondary_vfs = NULL; -#endif // Secondary part +typedef struct { + bool in_use; + int flags; +} fd_entry_t; -const static esp_vfs_fs_ops_t *primary_vfs = NULL; +typedef struct { + mux_entry_t entries[CONFIG_ESP_STDIO_MAX_VFS_ENTRIES]; + size_t used; -static vfs_console_context_t vfs_console = {0}; +#if !ESP_STDIO_IS_BASIC + fd_entry_t fds[CONFIG_ESP_STDIO_MAX_FDS]; +#endif -static size_t s_open_count = 0; + size_t fd_count; +} context_t; + +static context_t s_ctx = {0}; + +static const char *TAG = "esp_stdio"; + +static inline bool is_fd_valid(int fd) +{ +#if ESP_STDIO_IS_BASIC + return (fd == 0) && (s_ctx.fd_count > 0); +#else + return (fd >= 0) && (fd < CONFIG_ESP_STDIO_MAX_FDS) && s_ctx.fds[fd].in_use; +#endif +} + +static inline mux_entry_t *get_primary_entry(void) +{ + assert(s_ctx.used > 0); + return &s_ctx.entries[0]; +} + +#ifdef CONFIG_VFS_SUPPORT_TERMIOS +static const mux_entry_t *get_primary_termios_entry(int fd, const esp_vfs_termios_ops_t **out_ops) +{ + if (!is_fd_valid(fd)) { + errno = EBADF; + return NULL; + } + + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + + const esp_vfs_termios_ops_t *termios = entry->ops->termios; + if (!termios) { + errno = ENOSYS; + return NULL; + } + + if (out_ops) { + *out_ops = termios; + } + + return entry; +} +#endif // CONFIG_VFS_SUPPORT_TERMIOS + +static esp_err_t esp_stdio_register_sink(const esp_vfs_fs_ops_t *ops, int vfs_flags, void *vfs_ctx, const char *path, int fd) +{ + if (!ops || !path) { + return ESP_ERR_INVALID_ARG; + } + + if (s_ctx.used >= CONFIG_ESP_STDIO_MAX_VFS_ENTRIES) { + ESP_EARLY_LOGE(TAG, "Too many stdio sinks registered"); + return ESP_ERR_NO_MEM; + } + + int mask = ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC; + + if ((vfs_flags & mask) != mask) { + ESP_EARLY_LOGE(TAG, "The sink FS needs to use both context pointer api and be static allocated"); + return ESP_ERR_INVALID_ARG; + } + + if (!ops->write_p) { + ESP_EARLY_LOGE(TAG, "Stdio sink missing mandatory write handler"); + return ESP_ERR_INVALID_ARG; + } + + bool is_primary = (s_ctx.used == 0); + if (is_primary && (!ops->read_p || !ops->fstat_p || !ops->fcntl_p || !ops->fsync_p)) { + ESP_EARLY_LOGE(TAG, "Primary stdio sink missing mandatory read/fstat/fcntl/fsync handlers"); + return ESP_ERR_INVALID_ARG; + } + + size_t idx = s_ctx.used++; + + s_ctx.entries[idx] = (mux_entry_t) { + .ops = ops, + .vfs_flags = vfs_flags, + .vfs_ctx = vfs_ctx, + .path = path, + .fd = fd, + }; + + return ESP_OK; +} int console_open(__attribute__((unused)) void *ctx, const char * path, int flags, int mode) { - if (s_open_count > 0) { - // Underlying fd is already open, so just increment the open count - // and return the same fd - - s_open_count++; - return 0; +#if ESP_STDIO_IS_BASIC + (void) path; + s_ctx.fd_count++; + return 0; +#else + if (!path || strcmp(path, "/") != 0) { + errno = ENOENT; + return -1; } -// Primary port open -#if CONFIG_ESP_CONSOLE_UART - vfs_console.fd_primary = open("/dev/uart/"STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), flags, mode); -#elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG - vfs_console.fd_primary = open("/dev/usbserjtag", flags, mode); -#elif CONFIG_ESP_CONSOLE_USB_CDC - vfs_console.fd_primary = open("/dev/cdcacm", flags, mode); + if (s_ctx.fd_count >= CONFIG_ESP_STDIO_MAX_FDS) { + errno = ENFILE; + return -1; + } + + int fd = -1; + for (size_t i = 0; i < CONFIG_ESP_STDIO_MAX_FDS; ++i) { + if (!s_ctx.fds[i].in_use) { + fd = (int)i; + break; + } + } + + if (fd < 0) { + errno = ENFILE; + return -1; + } + + s_ctx.fd_count++; + + s_ctx.fds[fd] = (fd_entry_t) { + .in_use = true, + .flags = flags, + }; + + return fd; +#endif +} + +int console_close(__attribute__((unused)) void *ctx, int fd) +{ + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + +#if ESP_STDIO_IS_BASIC + s_ctx.fd_count--; #else - vfs_console.fd_primary = open("/dev/null", flags, mode); + s_ctx.fds[fd].in_use = false; + s_ctx.fd_count--; #endif -// Secondary port open -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - vfs_console.fd_secondary = open("/dev/secondary", flags, mode); -#endif - - s_open_count++; return 0; } ssize_t console_write(__attribute__((unused)) void *ctx, int fd, const void *data, size_t size) { - write(vfs_console.fd_primary, data, size); -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - write(vfs_console.fd_secondary, data, size); -#endif + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + for (size_t i = 0; i < s_ctx.used; i++) { + const mux_entry_t *entry = s_ctx.entries + i; + if (entry->ops->write_p && entry->fd >= 0) { + entry->ops->write_p(entry->vfs_ctx, entry->fd, data, size); + } + } + return size; } int console_fstat(__attribute__((unused)) void *ctx, int fd, struct stat * st) { - return fstat(vfs_console.fd_primary, st); -} - -int console_close(__attribute__((unused)) void *ctx, int fd) -{ - if (s_open_count == 0) { + if (!is_fd_valid(fd)) { errno = EBADF; return -1; } - s_open_count--; - - // We don't actually close the underlying fd until the open count reaches 0 - if (s_open_count > 0) { - return 0; + const mux_entry_t *entry = get_primary_entry(); + if (!entry->ops->fstat_p) { + errno = ENOSYS; + return -1; } - - // All function calls are to primary, except from write and close, which will be forwarded to both primary and secondary. - close(vfs_console.fd_primary); -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - close(vfs_console.fd_secondary); -#endif - return 0; + return entry->ops->fstat_p(entry->vfs_ctx, entry->fd, st); } ssize_t console_read(__attribute__((unused)) void *ctx, int fd, void * dst, size_t size) { - return read(vfs_console.fd_primary, dst, size); + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + + const mux_entry_t *entry = get_primary_entry(); + if (!entry->ops->read_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->read_p(entry->vfs_ctx, entry->fd, dst, size); } int console_fcntl(__attribute__((unused)) void *ctx, int fd, int cmd, int arg) { - return fcntl(vfs_console.fd_primary, cmd, arg); + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + + const mux_entry_t *entry = get_primary_entry(); + if (!entry->ops->fcntl_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->fcntl_p(entry->vfs_ctx, entry->fd, cmd, arg); } int console_fsync(__attribute__((unused)) void *ctx, int fd) { - const int ret_val = fsync(vfs_console.fd_primary); -#if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - (void)fsync(vfs_console.fd_secondary); -#endif + if (!is_fd_valid(fd)) { + errno = EBADF; + return -1; + } + + const mux_entry_t *primary = get_primary_entry(); + if (!primary->ops->fsync_p) { + errno = ENOSYS; + return -1; + } + + int ret_val = primary->ops->fsync_p(primary->vfs_ctx, primary->fd); + + for (size_t i = 1; i < s_ctx.used; i++) { + const mux_entry_t *entry = s_ctx.entries + i; + if (entry->ops->fsync_p && entry->fd >= 0) { + entry->ops->fsync_p(entry->vfs_ctx, entry->fd); + } + } + return ret_val; } #ifdef CONFIG_VFS_SUPPORT_DIR int console_access(__attribute__((unused)) void *ctx, const char *path, int amode) { - // currently only UART support DIR. - return access("/dev/uart/"STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), amode); + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + if (!entry->ops->dir || !entry->ops->dir->access_p) { + errno = ENOSYS; + return -1; + } + (void) path; + return entry->ops->dir->access_p(entry->vfs_ctx, entry->path, amode); } #endif // CONFIG_VFS_SUPPORT_DIR @@ -156,8 +326,10 @@ static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefd 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 - if (primary_vfs->select->start_select) { - return primary_vfs->select->start_select(nfds, readfds, writefds, exceptfds, select_sem, end_select_args); + 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); } return ESP_ERR_NOT_SUPPORTED; @@ -166,8 +338,10 @@ static esp_err_t console_start_select(int nfds, fd_set *readfds, fd_set *writefd 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 (primary_vfs->select->end_select) { - return primary_vfs->select->end_select(end_select_args); + const mux_entry_t *entry = get_primary_entry(); + assert(entry); + if (entry->fd >= 0 && entry->ops->select && entry->ops->select->end_select) { + return entry->ops->select->end_select(end_select_args); } return ESP_ERR_NOT_SUPPORTED; @@ -179,22 +353,58 @@ esp_err_t console_end_select(void *end_select_args) int console_tcsetattr(__attribute__((unused)) void *ctx, int fd, int optional_actions, const struct termios *p) { - return tcsetattr(vfs_console.fd_primary, optional_actions, p); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcsetattr_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcsetattr_p(entry->vfs_ctx, entry->fd, optional_actions, p); } int console_tcgetattr(__attribute__((unused)) void *ctx, int fd, struct termios *p) { - return tcgetattr(vfs_console.fd_primary, p); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcgetattr_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcgetattr_p(entry->vfs_ctx, entry->fd, p); } int console_tcdrain(__attribute__((unused)) void *ctx, int fd) { - return tcdrain(vfs_console.fd_primary); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcdrain_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcdrain_p(entry->vfs_ctx, entry->fd); } int console_tcflush(__attribute__((unused)) void *ctx, int fd, int select) { - return tcflush(vfs_console.fd_primary, select); + const esp_vfs_termios_ops_t *termios = NULL; + const mux_entry_t *entry = get_primary_termios_entry(fd, &termios); + if (!entry) { + return -1; + } + if (!termios->tcflush_p) { + errno = ENOSYS; + return -1; + } + return entry->ops->termios->tcflush_p(entry->vfs_ctx, entry->fd, select); } #endif // CONFIG_VFS_SUPPORT_TERMIOS @@ -242,31 +452,82 @@ static const esp_vfs_fs_ops_t s_vfs_console = { #endif // CONFIG_VFS_SUPPORT_TERMIOS }; -static esp_err_t esp_vfs_dev_console_register(void) -{ - return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); -} - esp_err_t esp_stdio_register(void) { esp_err_t err = ESP_OK; // Primary vfs part. #if CONFIG_ESP_CONSOLE_UART - primary_vfs = esp_vfs_uart_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_uart_get_vfs(); + int fd = ops->open_p(NULL, "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary UART sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/" STRINGIFY(CONFIG_ESP_CONSOLE_UART_NUM), + fd); #elif CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG - primary_vfs = esp_vfs_usb_serial_jtag_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_usb_serial_jtag_get_vfs(); + int fd = ops->open_p(NULL, "/", O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary USB JTAG sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd); #elif CONFIG_ESP_CONSOLE_USB_CDC - primary_vfs = esp_vfs_cdcacm_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_cdcacm_get_vfs(); + int fd = ops->open_p(NULL, "/", O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary USB CDC sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd); #else - primary_vfs = esp_vfs_null_get_vfs(); + const esp_vfs_fs_ops_t *ops = esp_vfs_null_get_vfs(); + int fd = ops->open_p(NULL, "/", O_RDWR, 0); + if (fd < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open primary null sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd); #endif + if (err != ESP_OK) { + return err; + } + // Secondary vfs part. #if CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG - secondary_vfs = esp_vfs_usb_serial_jtag_get_vfs(); + const esp_vfs_fs_ops_t *ops_secondary = esp_vfs_usb_serial_jtag_get_vfs(); + int fd_secondary = ops_secondary->open_p(NULL, "/", O_RDWR, 0); + if (fd_secondary < 0) { + ESP_EARLY_LOGE(TAG, "Failed to open secondary USB JTAG sink, errno=%d", errno); + return ESP_FAIL; + } + err = esp_stdio_register_sink(ops_secondary, + ESP_VFS_FLAG_CONTEXT_PTR | ESP_VFS_FLAG_STATIC, + NULL, + "/", + fd_secondary); + if (err != ESP_OK) { + return err; + } #endif - err = esp_vfs_dev_console_register(); - return err; + return esp_vfs_register_fs(ESP_VFS_DEV_CONSOLE, &s_vfs_console, ESP_VFS_FLAG_STATIC | ESP_VFS_FLAG_CONTEXT_PTR, NULL); } ESP_SYSTEM_INIT_FN(init_vfs_console, CORE, BIT(0), 119)