This is an automated email from the ASF dual-hosted git repository. acassis pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nuttx.git
commit d61c8b8e073aebc261b0f592dfb61505b95d9302 Author: Marco Casaroli <[email protected]> AuthorDate: Sat Oct 17 20:15:58 2026 +0200 libs/libc: Add popen(), pclose(), dpopen() and dpclose(). <stdio.h> declares popen() and pclose(), and <unistd.h> declares dpopen() and dpclose(), but libc did not have them. Only the apps/system/popen library had them, so a program that links with libc only could not use them. Also pclose() in that library returned the result of fclose(), not the termination status of the command. This commit adds the four functions to libc. dpopen() creates a pipe (or a socket pair for O_RDWR) and starts "sh -c command" with posix_spawn(). With CONFIG_LIBC_ENVPATH, PATH gives the location of "sh", otherwise _PATH_BSHELL is used. If there is no shell, dpopen() splits the command at blanks and starts the program in the first word directly, without shell syntax. popen() wraps the descriptor in a stream with fopencookie(). The descriptor is close-on-exec, so a later command does not get the streams of earlier popen() calls. pclose() closes the stream and returns the termination status from waitpid(). If the command already exited, the status is available only with CONFIG_SCHED_CHILD_STATUS, as for every waitpid() call. The functions need pipes, posix_spawn() and waitpid() (CONFIG_PIPES, CONFIG_LIBC_EXECFUNCS and CONFIG_SCHED_WAITPID). When CONFIG_SYSTEM_POPEN is set, libc does not build them, so the copy in apps/system/popen stays in use and the two copies do not clash. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Marco Casaroli <[email protected]> --- include/stdio.h | 4 +- include/unistd.h | 4 +- libs/libc/stdio/CMakeLists.txt | 10 ++ libs/libc/stdio/Make.defs | 9 ++ libs/libc/stdio/lib_popen.c | 204 +++++++++++++++++++++++++++ libs/libc/unistd/CMakeLists.txt | 10 ++ libs/libc/unistd/Make.defs | 9 ++ libs/libc/unistd/lib_dpopen.c | 302 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 548 insertions(+), 4 deletions(-) diff --git a/include/stdio.h b/include/stdio.h index 0061ee435ec..5df99b732f3 100644 --- a/include/stdio.h +++ b/include/stdio.h @@ -376,8 +376,8 @@ int remove(FAR const char *path); FAR char *ctermid(FAR char *s); -/* Shell operations. These are not actually implemented in the OS. See - * apps/system/popen for implementation. +/* Shell operations. libc has them when pipes, posix_spawn() and + * waitpid() are enabled. */ #ifndef __KERNEL__ diff --git a/include/unistd.h b/include/unistd.h index 8ab4726711d..2a7b7c550fc 100644 --- a/include/unistd.h +++ b/include/unistd.h @@ -425,8 +425,8 @@ FAR void *sbrk(intptr_t incr); int pipe(int pipefd[2]); int pipe2(int pipefd[2], int flags); -/* Pipe-based process I/O. These are not actually implemented in the OS. - * See apps/system/popen for implementation. +/* Pipe-based process I/O. libc has them when pipes, posix_spawn() and + * waitpid() are enabled. */ #ifndef __KERNEL__ diff --git a/libs/libc/stdio/CMakeLists.txt b/libs/libc/stdio/CMakeLists.txt index 47acb2cde28..e6236ee83eb 100644 --- a/libs/libc/stdio/CMakeLists.txt +++ b/libs/libc/stdio/CMakeLists.txt @@ -109,6 +109,16 @@ if(CONFIG_FILE_STREAM) lib_getwc.c lib_ungetwc.c lib_stdio_ext.c) + + # popen() needs dpopen(). CONFIG_SYSTEM_POPEN selects the copy in + # apps/system/popen instead. + + if(CONFIG_PIPES + AND CONFIG_LIBC_EXECFUNCS + AND CONFIG_SCHED_WAITPID + AND NOT CONFIG_SYSTEM_POPEN) + list(APPEND SRCS lib_popen.c) + endif() endif() target_sources(c PRIVATE ${SRCS}) diff --git a/libs/libc/stdio/Make.defs b/libs/libc/stdio/Make.defs index 99bdca23818..183b5852720 100644 --- a/libs/libc/stdio/Make.defs +++ b/libs/libc/stdio/Make.defs @@ -47,6 +47,15 @@ CSRCS += lib_setbuf.c lib_setvbuf.c lib_libfilelock.c lib_libgetstreams.c CSRCS += lib_setbuffer.c lib_fputwc.c lib_putwc.c lib_fputws.c CSRCS += lib_fopencookie.c lib_fmemopen.c lib_open_memstream.c lib_fgetwc.c CSRCS += lib_getwc.c lib_ungetwc.c lib_stdio_ext.c + +# popen() needs dpopen(). CONFIG_SYSTEM_POPEN selects the copy in +# apps/system/popen instead. + +ifeq ($(CONFIG_PIPES)$(CONFIG_LIBC_EXECFUNCS)$(CONFIG_SCHED_WAITPID),yyy) +ifneq ($(CONFIG_SYSTEM_POPEN),y) +CSRCS += lib_popen.c +endif +endif endif # Add the stdio directory to the build diff --git a/libs/libc/stdio/lib_popen.c b/libs/libc/stdio/lib_popen.c new file mode 100644 index 00000000000..cd930a9f5ea --- /dev/null +++ b/libs/libc/stdio/lib_popen.c @@ -0,0 +1,204 @@ +/**************************************************************************** + * libs/libc/stdio/lib_popen.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> + +#include <sys/wait.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "libc.h" + +/* The kernel side of libc does not run commands */ + +#ifndef __KERNEL__ + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* The cookie of a stream from popen() */ + +struct popen_s +{ + int fd; /* The pipe to the command */ + pid_t pid; /* The process ID of the shell */ +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static ssize_t popen_read(FAR void *cookie, FAR char *buf, size_t size) +{ + FAR struct popen_s *pp = cookie; + + return read(pp->fd, buf, size); +} + +static ssize_t popen_write(FAR void *cookie, FAR const char *buf, + size_t size) +{ + FAR struct popen_s *pp = cookie; + + return write(pp->fd, buf, size); +} + +static off_t popen_seek(FAR void *cookie, FAR off_t *offset, int whence) +{ + set_errno(ESPIPE); + return -1; +} + +static int popen_close(FAR void *cookie) +{ + FAR struct popen_s *pp = cookie; + int ret; + + ret = close(pp->fd); + lib_free(pp); + return ret; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: popen + * + * Description: + * Run the command "sh -c command" in a new process, with a pipe between + * the caller and the command. With mode "r", the caller reads the + * standard output of the command from the returned stream. With mode + * "w", the caller writes the standard input of the command to the + * returned stream. The command does not get the streams of earlier + * popen() calls. + * + * Input Parameters: + * command - The command string for the shell. + * mode - "r" or "w". An "e" is accepted and ignored: the descriptor + * is always close-on-exec. + * + * Returned Value: + * A stream on success. On failure, NULL is returned and errno is set. + * + ****************************************************************************/ + +FAR FILE *popen(FAR const char *command, FAR const char *mode) +{ + cookie_io_functions_t io = + { + popen_read, popen_write, popen_seek, popen_close + }; + + FAR struct popen_s *pp; + FAR FILE *stream; + int oflag; + int errcode; + + if (mode == NULL || (mode[0] != 'r' && mode[0] != 'w') || + strchr(mode, '+') != NULL) + { + set_errno(EINVAL); + return NULL; + } + + oflag = mode[0] == 'r' ? O_RDONLY : O_WRONLY; + + pp = lib_malloc(sizeof(*pp)); + if (pp == NULL) + { + set_errno(ENOMEM); + return NULL; + } + + pp->fd = dpopen(command, oflag | O_CLOEXEC, &pp->pid); + if (pp->fd < 0) + { + lib_free(pp); + return NULL; + } + + stream = fopencookie(pp, mode[0] == 'r' ? "r" : "w", io); + if (stream == NULL) + { + errcode = get_errno(); + dpclose(pp->fd, pp->pid); + lib_free(pp); + set_errno(errcode); + } + + return stream; +} + +/**************************************************************************** + * Name: pclose + * + * Description: + * Close a stream from popen() and wait for the command to terminate. + * + * Input Parameters: + * stream - The stream from popen(). + * + * Returned Value: + * The termination status of the shell, as from waitpid(). On failure, + * -1 is returned and errno is set. + * + ****************************************************************************/ + +int pclose(FAR FILE *stream) +{ + FAR struct popen_s *pp; + pid_t pid; + int status; + int ret; + + if (stream == NULL || stream->fs_iofunc.close != popen_close) + { + set_errno(EINVAL); + return -1; + } + + /* fclose() frees the cookie, so keep the process ID */ + + pp = stream->fs_cookie; + pid = pp->pid; + fclose(stream); + + do + { + ret = waitpid(pid, &status, 0); + } + while (ret < 0 && get_errno() == EINTR); + + return ret < 0 ? -1 : status; +} + +#endif /* __KERNEL__ */ diff --git a/libs/libc/unistd/CMakeLists.txt b/libs/libc/unistd/CMakeLists.txt index 7cf686154d7..06ed405a890 100644 --- a/libs/libc/unistd/CMakeLists.txt +++ b/libs/libc/unistd/CMakeLists.txt @@ -129,4 +129,14 @@ if(CONFIG_PIPES) list(APPEND SRCS lib_pipe.c) endif() +# dpopen() needs pipes, posix_spawn() and waitpid(). CONFIG_SYSTEM_POPEN +# selects the copy in apps/system/popen instead. + +if(CONFIG_PIPES + AND CONFIG_LIBC_EXECFUNCS + AND CONFIG_SCHED_WAITPID + AND NOT CONFIG_SYSTEM_POPEN) + list(APPEND SRCS lib_dpopen.c) +endif() + target_sources(c PRIVATE ${SRCS}) diff --git a/libs/libc/unistd/Make.defs b/libs/libc/unistd/Make.defs index 67abbc43963..20a4e048786 100644 --- a/libs/libc/unistd/Make.defs +++ b/libs/libc/unistd/Make.defs @@ -78,6 +78,15 @@ ifeq ($(CONFIG_PIPES),y) CSRCS += lib_pipe.c endif +# dpopen() needs pipes, posix_spawn() and waitpid(). CONFIG_SYSTEM_POPEN +# selects the copy in apps/system/popen instead. + +ifeq ($(CONFIG_PIPES)$(CONFIG_LIBC_EXECFUNCS)$(CONFIG_SCHED_WAITPID),yyy) +ifneq ($(CONFIG_SYSTEM_POPEN),y) +CSRCS += lib_dpopen.c +endif +endif + # Add the unistd directory to the build DEPPATH += --dep-path unistd diff --git a/libs/libc/unistd/lib_dpopen.c b/libs/libc/unistd/lib_dpopen.c new file mode 100644 index 00000000000..3100eb4b2da --- /dev/null +++ b/libs/libc/unistd/lib_dpopen.c @@ -0,0 +1,302 @@ +/**************************************************************************** + * libs/libc/unistd/lib_dpopen.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include <nuttx/config.h> + +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <errno.h> +#include <fcntl.h> +#include <paths.h> +#include <spawn.h> +#include <string.h> +#include <unistd.h> + +#if defined(CONFIG_NET_LOCAL) && defined(CONFIG_NET_LOCAL_STREAM) +# include <sys/socket.h> +#endif + +#include "libc.h" + +/* The kernel side of libc does not run commands */ + +#ifndef __KERNEL__ + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +/* The shell that runs the command. With CONFIG_LIBC_ENVPATH, PATH gives + * the location of "sh". + */ + +#ifdef CONFIG_LIBC_ENVPATH +# define DPOPEN_SHELL "sh" +#else +# define DPOPEN_SHELL _PATH_BSHELL +#endif + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: dpopen_direct + * + * Description: + * Start a command without a shell, for a system that has none: split the + * command at blanks and start the program in the first word with + * posix_spawnp(). Shell syntax (pipes, redirection, quotes, variables) + * is not supported. + * + ****************************************************************************/ + +static int dpopen_direct(FAR pid_t *pid, FAR const char *command, + FAR const posix_spawn_file_actions_t *actions) +{ + FAR char **argv; + FAR char *saveptr; + FAR char *words; + size_t len = strlen(command); + size_t nargs = len / 2 + 2; /* A word needs a character and a blank */ + size_t argc = 0; + int errcode; + + argv = lib_malloc(nargs * sizeof(FAR char *) + len + 1); + if (argv == NULL) + { + return ENOMEM; + } + + words = (FAR char *)&argv[nargs]; + memcpy(words, command, len + 1); + + argv[0] = strtok_r(words, " \t\n", &saveptr); + while (argv[argc] != NULL) + { + argv[++argc] = strtok_r(NULL, " \t\n", &saveptr); + } + + /* posix_spawnp() copies the arguments, so they can be freed after it */ + + errcode = argc > 0 ? posix_spawnp(pid, argv[0], actions, NULL, argv, + NULL) : EINVAL; + lib_free(argv); + return errcode; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: dpopen + * + * Description: + * Run the command "sh -c command" in a new process and return a file + * descriptor connected to it. If there is no shell, the command is + * split at blanks and its first word is started directly, without shell + * syntax. + * + * This is the descriptor counterpart of popen(), as dprintf() is the + * counterpart of fprintf(). + * + * Input Parameters: + * command - The command string for the shell. + * oflag - O_RDONLY to read the standard output of the command, + * O_WRONLY to write its standard input, or O_RDWR for both + * (needs local stream sockets). O_CLOEXEC sets the + * close-on-exec flag of the returned descriptor. + * pid - The location that receives the process ID of the shell. + * + * Returned Value: + * A file descriptor on success. On failure, -1 is returned and errno is + * set. + * + ****************************************************************************/ + +int dpopen(FAR const char *command, int oflag, FAR pid_t *pid) +{ + posix_spawn_file_actions_t file_actions; + FAR char *argv[4]; + int fd[2]; + int childfd; + int parentfd; + int errcode; + + if (command == NULL || pid == NULL) + { + set_errno(EINVAL); + return -1; + } + + /* Create a pipe or a socket pair. fd[0] is the read end of the pipe + * and fd[1] is the write end. + */ + + if ((oflag & O_ACCMODE) == O_RDWR) + { +#if defined(CONFIG_NET_LOCAL) && defined(CONFIG_NET_LOCAL_STREAM) + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fd) < 0) + { + return -1; + } + + childfd = fd[0]; + parentfd = fd[1]; +#else + set_errno(ENOTSUP); + return -1; +#endif + } + else if ((oflag & O_ACCMODE) != O_RDONLY && + (oflag & O_ACCMODE) != O_WRONLY) + { + set_errno(EINVAL); + return -1; + } + else if (pipe2(fd, O_CLOEXEC) < 0) + { + return -1; + } + else if ((oflag & O_ACCMODE) == O_RDONLY) + { + childfd = fd[1]; /* The command writes to the pipe */ + parentfd = fd[0]; /* The caller reads from the pipe */ + } + else + { + childfd = fd[0]; /* The command reads from the pipe */ + parentfd = fd[1]; /* The caller writes to the pipe */ + } + + errcode = posix_spawn_file_actions_init(&file_actions); + if (errcode != 0) + { + goto errout_with_pipe; + } + + /* Connect the standard input or output of the command to the pipe */ + + if ((oflag & O_ACCMODE) != O_WRONLY) + { + errcode = posix_spawn_file_actions_adddup2(&file_actions, childfd, + STDOUT_FILENO); + if (errcode != 0) + { + goto errout_with_actions; + } + } + + if ((oflag & O_ACCMODE) != O_RDONLY) + { + errcode = posix_spawn_file_actions_adddup2(&file_actions, childfd, + STDIN_FILENO); + if (errcode != 0) + { + goto errout_with_actions; + } + } + + argv[0] = "sh"; + argv[1] = "-c"; + argv[2] = (FAR char *)command; + argv[3] = NULL; + + errcode = posix_spawn(pid, DPOPEN_SHELL, &file_actions, NULL, argv, + NULL); + if (errcode == ENOENT) + { + /* There is no shell: start the command directly */ + + errcode = dpopen_direct(pid, command, &file_actions); + } + + posix_spawn_file_actions_destroy(&file_actions); + if (errcode != 0) + { + goto errout_with_pipe; + } + + /* The other ends of earlier pipes are close-on-exec, so a new command + * does not keep them open. Clear the flag only if the caller wants. + */ + + if ((oflag & O_CLOEXEC) == 0) + { + ioctl(parentfd, FIONCLEX, 0); + } + + close(childfd); + return parentfd; + +errout_with_actions: + posix_spawn_file_actions_destroy(&file_actions); + +errout_with_pipe: + close(fd[0]); + close(fd[1]); + set_errno(errcode); + return -1; +} + +/**************************************************************************** + * Name: dpclose + * + * Description: + * Close a file descriptor from dpopen() and wait for the command to + * terminate. + * + * Input Parameters: + * fd - The file descriptor from dpopen(). + * pid - The process ID from dpopen(). + * + * Returned Value: + * The termination status of the command, as from waitpid(). On + * failure, -1 is returned and errno is set. + * + ****************************************************************************/ + +int dpclose(int fd, pid_t pid) +{ + int status; + int ret; + + if (fd >= 0) + { + close(fd); + } + + do + { + ret = waitpid(pid, &status, 0); + } + while (ret < 0 && get_errno() == EINTR); + + return ret < 0 ? -1 : status; +} + +#endif /* __KERNEL__ */
