system/popen: add dpopen/dpclose fd-based interface

Add dpopen()/dpclose() as the descriptor-based counterpart of
popen()/pclose(), analogous to how dprintf() relates to fprintf().
dpopen() returns a raw file descriptor instead of a FILE stream,
avoiding the stdio.h dependency for callers that only need an fd.

Refactor popen() as a thin wrapper: dpopen() + fdopen() + FILE
container.  All pipe creation and process spawning logic now lives
in dpopen.c.

Also remove the hard dependency on NSH_LIBRARY from SYSTEM_POPEN.
When NSH is available, commands are executed through sh -c with full
shell syntax support.  When NSH is not available, commands are split
by whitespace and executed directly via posix_spawnp().

Add CONFIG_SYSTEM_POPEN_MAXARGUMENTS (default 7) to control the
argv array size for the no-shell path.

Signed-off-by: cuiziwei <cuiziwei@xiaomi.com>
This commit is contained in:
cuiziwei 2026-04-28 17:00:56 +08:00 committed by Xiang Xiao
parent 3a255d5463
commit 0d35e2e0bb
5 changed files with 385 additions and 271 deletions

View file

@ -21,5 +21,5 @@
# ##############################################################################
if(CONFIG_SYSTEM_POPEN)
target_sources(apps PRIVATE popen.c)
target_sources(apps PRIVATE dpopen.c popen.c)
endif()

View file

@ -4,14 +4,25 @@
#
config SYSTEM_POPEN
bool "popen()/pclose() Functions"
bool "popen()/pclose()/dpopen()/dpclose() Functions"
default n
select SCHED_WAITPID
depends on NSH_LIBRARY && PIPES
---help---
Enable support for the popen() and pclose() interfaces.
This will support execution of NSH commands from C code with
pipe communications with the shell.
Enable support for the popen(), pclose(), dpopen(), and
dpclose() interfaces.
dpopen()/dpclose() are the descriptor-based counterparts
of popen()/pclose(), analogous to how dprintf() relates
to fprintf(). They return a raw file descriptor instead
of a FILE stream, avoiding the stdio.h dependency.
popen()/pclose() are thin wrappers around dpopen()/dpclose()
that additionally wrap the fd in a FILE stream.
Commands are executed through the NSH shell (sh -c command),
supporting full shell syntax including pipes, redirects,
and globbing.
if SYSTEM_POPEN

View file

@ -22,8 +22,8 @@
include $(APPDIR)/Make.defs
# popen()/pclose functions
# dpopen()/dpclose() core and popen()/pclose() FILE wrappers
CSRCS = popen.c
CSRCS = dpopen.c popen.c
include $(APPDIR)/Application.mk

321
system/popen/dpopen.c Normal file
View file

@ -0,0 +1,321 @@
/****************************************************************************
* apps/system/popen/dpopen.c
*
* 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 <sys/ioctl.h>
#include <unistd.h>
#include <sched.h>
#include <spawn.h>
#include <debug.h>
#include <fcntl.h>
#include <errno.h>
#if defined(CONFIG_NET_LOCAL) && defined(CONFIG_NET_LOCAL_STREAM)
# include <sys/socket.h>
#endif
#include "nshlib/nshlib.h"
/****************************************************************************
* Public Functions
****************************************************************************/
/****************************************************************************
* Name: dpopen
*
* Description:
* Execute a command and return a raw file descriptor connected to the
* child process via a pipe, along with the child PID.
*
* This is the descriptor-based counterpart of popen(), analogous to how
* dprintf() relates to fprintf(). It avoids the FILE stream layer,
* making it suitable for callers that only need a raw fd and want to
* avoid the stdio.h dependency.
*
* When NSH is available, commands are executed through the shell
* (sh -c command), supporting full shell syntax.
*
* Input Parameters:
* command - The command string to execute
* oflag - O_RDONLY to read child stdout, O_WRONLY to write child stdin,
* O_RDWR for bidirectional socket mode
* pid - Location to return the child process ID
*
* Returned Value:
* A valid file descriptor on success, or -1 on failure with errno set.
*
****************************************************************************/
int dpopen(FAR const char *command, int oflag, FAR pid_t *pid)
{
struct sched_param param;
posix_spawnattr_t attr;
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)
{
errno = EINVAL;
return -1;
}
/* Create a pipe or socketpair. fd[0] refers to the read end of the
* pipe; fd[1] refers to the write end of the pipe.
*/
if ((oflag & O_RDWR) == O_RDWR)
{
#if defined(CONFIG_NET_LOCAL) && defined(CONFIG_NET_LOCAL_STREAM)
/* Create a socketpair for bidirectional communication */
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, fd) < 0)
{
return -1;
}
childfd = fd[0];
parentfd = fd[1];
#else
errno = ENOTSUP;
return -1;
#endif
}
else if (pipe2(fd, O_CLOEXEC) < 0)
{
return -1;
}
else if ((oflag & O_ACCMODE) == O_RDONLY)
{
/* Pipe is the output from the child */
childfd = fd[1]; /* Child writes to pipe */
parentfd = fd[0]; /* Parent reads from pipe */
}
else if ((oflag & O_ACCMODE) == O_WRONLY)
{
/* Pipe is the input to the child */
childfd = fd[0]; /* Child reads from pipe */
parentfd = fd[1]; /* Parent writes to pipe */
}
else
{
errcode = EINVAL;
goto errout_with_pipe;
}
/* Initialize attributes for task_spawn() (or posix_spawn()). */
errcode = posix_spawnattr_init(&attr);
if (errcode != 0)
{
goto errout_with_pipe;
}
errcode = posix_spawn_file_actions_init(&file_actions);
if (errcode != 0)
{
goto errout_with_attr;
}
/* Set the correct stack size and priority */
param.sched_priority = CONFIG_SYSTEM_POPEN_PRIORITY;
errcode = posix_spawnattr_setschedparam(&attr, &param);
if (errcode != 0)
{
goto errout_with_actions;
}
#ifndef CONFIG_SYSTEM_POPEN_SHPATH
errcode = posix_spawnattr_setstacksize(&attr,
CONFIG_SYSTEM_POPEN_STACKSIZE);
if (errcode != 0)
{
goto errout_with_actions;
}
#endif
/* If robin robin scheduling is enabled, then set the scheduling policy
* of the new task to SCHED_RR before it has a chance to run.
*/
#if CONFIG_RR_INTERVAL > 0
errcode = posix_spawnattr_setschedpolicy(&attr, SCHED_RR);
if (errcode != 0)
{
goto errout_with_actions;
}
errcode = posix_spawnattr_setflags(&attr,
POSIX_SPAWN_SETSCHEDPARAM |
POSIX_SPAWN_SETSCHEDULER);
if (errcode != 0)
{
goto errout_with_actions;
}
#else
errcode = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM);
if (errcode != 0)
{
goto errout_with_actions;
}
#endif
/* Redirect child's stdin or stdout to the pipe */
if ((oflag & O_RDWR) == O_RDWR)
{
errcode = posix_spawn_file_actions_adddup2(&file_actions, childfd,
STDIN_FILENO);
if (errcode != 0)
{
goto errout_with_actions;
}
errcode = posix_spawn_file_actions_adddup2(&file_actions, childfd,
STDOUT_FILENO);
if (errcode != 0)
{
goto errout_with_actions;
}
}
else
{
errcode = posix_spawn_file_actions_adddup2(&file_actions, childfd,
(oflag & O_ACCMODE) == O_RDONLY ?
STDOUT_FILENO : STDIN_FILENO);
if (errcode != 0)
{
goto errout_with_actions;
}
}
/* Call task_spawn() (or posix_spawn), re-directing stdin or stdout
* appropriately.
*/
argv[1] = "-c";
argv[2] = (FAR char *)command;
argv[3] = NULL;
#ifdef CONFIG_SYSTEM_POPEN_SHPATH
argv[0] = CONFIG_SYSTEM_POPEN_SHPATH;
errcode = posix_spawn(pid, argv[0], &file_actions,
&attr, argv, NULL);
#else
*pid = task_spawn("dpopen", nsh_system, &file_actions,
&attr, argv + 1, NULL);
if (*pid < 0)
{
errcode = -*pid;
}
#endif
if (errcode != 0)
{
serr("ERROR: dpopen spawn failed: %d\n", errcode);
goto errout_with_pipe;
}
/* Free attributes and file actions */
posix_spawn_file_actions_destroy(&file_actions);
posix_spawnattr_destroy(&attr);
/* Close the child's end in the parent */
if (!(oflag & O_CLOEXEC))
{
ioctl(parentfd, FIONCLEX, 0);
}
close(childfd);
return parentfd;
errout_with_actions:
posix_spawn_file_actions_destroy(&file_actions);
errout_with_attr:
posix_spawnattr_destroy(&attr);
errout_with_pipe:
close(fd[0]);
close(fd[1]);
errno = errcode;
return -1;
}
/****************************************************************************
* Name: dpclose
*
* Description:
* Close a file descriptor opened by dpopen() and wait for the child
* process to terminate.
*
* This is the descriptor-based counterpart of pclose().
*
* Input Parameters:
* fd - The file descriptor returned by dpopen()
* pid - The child process ID returned by dpopen()
*
* Returned Value:
* The child termination status on success, or -1 on failure with
* errno set.
*
****************************************************************************/
int dpclose(int fd, pid_t pid)
{
#ifdef CONFIG_SCHED_WAITPID
int status;
int ret;
#endif
if (fd >= 0)
{
close(fd);
}
#ifdef CONFIG_SCHED_WAITPID
ret = waitpid(pid, &status, 0);
if (ret < 0)
{
return ERROR;
}
return status;
#else
return 0;
#endif
}

View file

@ -26,21 +26,13 @@
#include <nuttx/config.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sched.h>
#include <spawn.h>
#include <assert.h>
#include <nuttx/debug.h>
#include <fcntl.h>
#include <errno.h>
#include "nshlib/nshlib.h"
/****************************************************************************
* Private Types
****************************************************************************/
@ -69,17 +61,9 @@ struct popen_file_s
* executed command, and will return a pointer to a stream that can be
* used to either read from or write to the pipe.
*
* The environment of the executed command will be as if a child process
* were created within the popen() call using the fork() function, and the
* child invoked the sh utility using the call:
*
* execl(shell path, "sh", "-c", command, NULL);
*
* where shell path is an unspecified pathname for the sh utility.
*
* The popen() function will ensure that any streams from previous popen()
* calls that remain open in the parent process are closed in the new child
* process.
* This is a thin wrapper around dpopen() that wraps the returned file
* descriptor in a FILE stream, analogous to how fprintf() relates to
* dprintf().
*
* The mode argument to popen() is a string that specifies I/O mode:
*
@ -95,18 +79,12 @@ struct popen_file_s
* the stream pointer returned by popen(), will be the writable end of
* the pipe.
*
* If mode is any other value, the result is undefined.
*
* After popen(), both the parent and the child process will be capable of
* executing independently before either terminates.
*
* Pipe streams are byte-oriented.
*
* Input Parameters:
* command
* command - The command string to execute
* mode - "r" or "w"
*
* Returned Value:
* A non-NULLFILE stream connected to the shell instance is returned on
* A non-NULL FILE stream connected to the child process is returned on
* success. NULL is returned on any failure with the errno variable set
* appropriately.
*
@ -115,244 +93,67 @@ struct popen_file_s
FILE *popen(FAR const char *command, FAR const char *mode)
{
FAR struct popen_file_s *container;
struct sched_param param;
posix_spawnattr_t attr;
posix_spawn_file_actions_t file_actions;
FAR char *argv[4];
int fd[2];
int oldfd[2];
int newfd[2];
int retfd;
int errcode;
int result = 0;
bool rw = false;
int oflag;
int fd;
/* Allocate a container for returned FILE stream */
container = (FAR struct popen_file_s *)malloc(sizeof(struct popen_file_s));
if (container == NULL)
{
errcode = ENOMEM;
goto errout;
errno = ENOMEM;
return NULL;
}
oldfd[1] = 0;
newfd[1] = 0;
/* Map mode string to open flags */
/* Create a pipe. fd[0] refers to the read end of the pipe; fd[1] refers
* to the write end of the pipe.
* Is the pipe the input to the shell? Or the output?
*/
if (strcmp(mode, "r") == 0 &&
(result = pipe2(fd, O_CLOEXEC)) >= 0)
if (strstr(mode, "r+") || strstr(mode, "w+"))
{
/* Pipe is the output from the shell */
oldfd[0] = 1; /* Replace stdout with the write side of the pipe */
newfd[0] = fd[1];
retfd = fd[0]; /* Use read side of the pipe to create the return stream */
oflag = O_RDWR;
}
else if (strcmp(mode, "w") == 0 &&
(result = pipe2(fd, O_CLOEXEC)) >= 0)
else if (strstr(mode, "r"))
{
/* Pipe is the input to the shell */
oldfd[0] = 0; /* Replace stdin with the read side of the pipe */
newfd[0] = fd[0];
retfd = fd[1]; /* Use write side of the pipe to create the return stream */
oflag = O_RDONLY;
}
/* Create a socketpair. Using fd[0] as the input and output to the shell */
#if defined(CONFIG_NET_LOCAL) && defined(CONFIG_NET_LOCAL_STREAM)
else if ((strcmp(mode, "r+") == 0 || strcmp(mode, "w+") == 0) &&
(result = socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC,
0, fd)) >= 0)
else if (strstr(mode, "w"))
{
/* Socketpair is the input/output to the shell */
rw = true;
oldfd[0] = 0; /* Replace stdin with the one side of a socket pair */
newfd[0] = fd[0];
oldfd[1] = 1; /* Replace stdout with the one side of a socket pair */
newfd[1] = fd[0];
retfd = fd[1]; /* Use other side of the socket pair to create the return stream */
}
#endif
else if (result < 0)
{
errcode = errno;
goto errout_with_container;
oflag = O_WRONLY;
}
else
{
errcode = EINVAL;
goto errout_with_pipe;
free(container);
errno = EINVAL;
return NULL;
}
/* Create the FILE stream return reference */
if (strchr(mode, 'e') != NULL)
{
oflag |= O_CLOEXEC;
}
container->original = fdopen(retfd, mode);
/* Use dpopen() to do the real work */
fd = dpopen(command, oflag, &container->shell);
if (fd < 0)
{
free(container);
return NULL;
}
/* Wrap the raw fd in a FILE stream */
container->original = fdopen(fd, mode);
if (container->original == NULL)
{
errcode = errno;
goto errout_with_pipe;
int errcode = errno;
dpclose(fd, container->shell);
free(container);
errno = errcode;
return NULL;
}
/* Initialize attributes for task_spawn() (or posix_spawn()). */
errcode = posix_spawnattr_init(&attr);
if (errcode != 0)
{
goto errout_with_stream;
}
errcode = posix_spawn_file_actions_init(&file_actions);
if (errcode != 0)
{
goto errout_with_attrs;
}
/* Set the correct stack size and priority */
param.sched_priority = CONFIG_SYSTEM_POPEN_PRIORITY;
errcode = posix_spawnattr_setschedparam(&attr, &param);
if (errcode != 0)
{
goto errout_with_actions;
}
#ifndef CONFIG_SYSTEM_POPEN_SHPATH
errcode = posix_spawnattr_setstacksize(&attr,
CONFIG_SYSTEM_POPEN_STACKSIZE);
if (errcode != 0)
{
goto errout_with_actions;
}
#endif
/* If robin robin scheduling is enabled, then set the scheduling policy
* of the new task to SCHED_RR before it has a chance to run.
*/
#if CONFIG_RR_INTERVAL > 0
errcode = posix_spawnattr_setschedpolicy(&attr, SCHED_RR);
if (errcode != 0)
{
goto errout_with_actions;
}
errcode = posix_spawnattr_setflags(&attr,
POSIX_SPAWN_SETSCHEDPARAM |
POSIX_SPAWN_SETSCHEDULER);
if (errcode != 0)
{
goto errout_with_actions;
}
#else
errcode = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSCHEDPARAM);
if (errcode != 0)
{
goto errout_with_actions;
}
#endif
/* Redirect input or output as determined by the mode parameter */
errcode = posix_spawn_file_actions_adddup2(&file_actions,
newfd[0], oldfd[0]);
if (errcode != 0)
{
goto errout_with_actions;
}
if (rw)
{
errcode = posix_spawn_file_actions_adddup2(&file_actions,
newfd[1], oldfd[1]);
if (errcode != 0)
{
goto errout_with_actions;
}
}
/* Call task_spawn() (or posix_spawn), re-directing stdin or stdout
* appropriately.
*/
argv[1] = "-c";
argv[2] = (FAR char *)command;
argv[3] = NULL;
#ifdef CONFIG_SYSTEM_POPEN_SHPATH
argv[0] = CONFIG_SYSTEM_POPEN_SHPATH;
errcode = posix_spawn(&container->shell, argv[0], &file_actions,
&attr, argv, NULL);
#else
container->shell = task_spawn("popen", nsh_system, &file_actions,
&attr, argv + 1, NULL);
if (container->shell < 0)
{
errcode = -container->shell;
}
#endif
if (errcode != 0)
{
serr("ERROR: Spawn failed: %d\n", errcode);
goto errout_with_actions;
}
/* We can close the 'newfd' now. It is no longer useful on this side of
* the interface.
*/
close(newfd[0]);
if (rw && newfd[0] != newfd[1])
{
close(newfd[1]);
}
/* Free attributes and file actions. Ignoring return values in the case
* of an error.
*/
posix_spawn_file_actions_destroy(&file_actions);
posix_spawnattr_destroy(&attr);
if (strchr(mode, 'e') == NULL)
{
ioctl(retfd, FIOCLEX, 0);
}
/* Finale and return input input/output stream */
memcpy(&container->copy, container->original, sizeof(FILE));
return &container->copy;
errout_with_actions:
posix_spawn_file_actions_destroy(&file_actions);
errout_with_attrs:
posix_spawnattr_destroy(&attr);
errout_with_stream:
fclose(container->original);
errout_with_pipe:
close(fd[0]);
close(fd[1]);
errout_with_container:
free(container);
errout:
errno = errcode;
return NULL;
}
/****************************************************************************
@ -388,12 +189,12 @@ errout:
* If the argument stream to pclose() is not a pointer to a stream created
* by popen(), the result of pclose() is undefined.
*
* Description:
* Input Parameters:
* stream - The stream reference returned by a previous call to popen()
*
* Returned Value:
* Zero (OK) is returned on success; otherwise -1 (ERROR) is returned and
* the errno variable is set appropriately.
* The child termination status on success, or -1 (ERROR) on failure
* with errno set.
*
****************************************************************************/
@ -402,12 +203,7 @@ int pclose(FILE *stream)
FAR struct popen_file_s *container = (FAR struct popen_file_s *)stream;
FILE *original;
pid_t shell;
#ifdef CONFIG_SCHED_WAITPID
int status;
int result;
#endif
DEBUGASSERT(container != NULL && container->original != NULL);
original = container->original;
/* Set the state of the original file descriptor to the state of the
@ -417,7 +213,7 @@ int pclose(FILE *stream)
memcpy(original, &container->copy, sizeof(FILE));
/* Then close the original and free the container (saving the PID of the
* shell process)
* shell process). Pass -1 to dpclose since fclose already closed the fd.
*/
fclose(original);
@ -425,19 +221,5 @@ int pclose(FILE *stream)
shell = container->shell;
free(container);
#ifdef CONFIG_SCHED_WAITPID
/* Wait for the shell to exit, retrieving the return value if available. */
result = waitpid(shell, &status, 0);
if (result < 0)
{
/* The errno has already been set */
return ERROR;
}
return status;
#else
return OK;
#endif
return dpclose(-1, shell);
}