To do this, you need to use the low level facilities. You need to create an extra pipe, close-on-exec, that the child uses to write the error code when exec fails.
Since the pipe is close-on-exec, it will be closed by the kernel at the start of the execution of the new binary. (We do not actually know if it is running at that point; we only know that the exec did not fail. So, do not assume that a closed pipe means the command is already running. It only means it did not fail yet.)
The parent process closes the unnecessary pipe ends, and reads from the control pipe. If the read succeeds, the child process failed to execute the command, and the data read describes the error. If the pipe is closed (read returns 0), the command execution will start (there was no error barring its execution).
After that, we can continue reading from the pipe as usual. When the child process closes the pipe, we should use waitpid() to reap it.
Consider the following example program. It executes the command specified on the command line -- use sh -c 'command'
if you want the same behaviour as system()
and popen()
. (That is, pathname == "sh"
and argv == { "sh", "-c", "command", NULL }
.)
It reads the output from the command character by character, counting them, until the child ends (by closing the pipe). After that, we reap the child process, and report the status.
If the command could not be executed, the reason is also reported. (Since non-executables are reported as ENOENT
("No such file or directory"), the exec_subproc()
modifies that case to EACCESS
("Permission denied").)
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int reap_subproc(pid_t pid)
{
int status;
pid_t p;
if (pid < 1) {
errno = EINVAL;
return -1;
}
do {
status = 0;
p = waitpid(pid, &status, 0);
} while (p == -1 && errno == EINTR);
if (p == -1)
return -1;
errno = 0;
return status;
}
FILE *exec_subproc(pid_t *pidptr, const char *pathname, char **argv)
{
char buffer[1];
int datafd[2], controlfd[2], result;
FILE *out;
ssize_t n;
pid_t pid, p;
if (pidptr)
*pidptr = (pid_t)0;
if (!pidptr || !pathname || !*pathname || !argv || !argv[0]) {
errno = EINVAL;
return NULL;
}
if (pipe(datafd) == -1)
return NULL;
if (pipe(controlfd) == -1) {
const int saved_errno = errno;
close(datafd[0]);
close(datafd[1]);
errno = saved_errno;
return NULL;
}
if (fcntl(datafd[0], F_SETFD, FD_CLOEXEC) == -1 ||
fcntl(controlfd[1], F_SETFD, FD_CLOEXEC) == -1) {
const int saved_errno = errno;
close(datafd[0]);
close(datafd[1]);
close(controlfd[0]);
close(controlfd[1]);
errno = saved_errno;
return NULL;
}
pid = fork();
if (pid == (pid_t)-1) {
const int saved_errno = errno;
close(datafd[0]);
close(datafd[1]);
close(controlfd[0]);
close(controlfd[1]);
errno = saved_errno;
return NULL;
}
if (!pid) {
/* Child process. */
close(datafd[0]);
close(controlfd[0]);
if (datafd[1] != STDOUT_FILENO) {
do {
result = dup2(datafd[1], STDOUT_FILENO);
} while (result == -1 && errno == EINTR);
if (result == -1) {
buffer[0] = errno;
close(datafd[1]);
do {
n = write(controlfd[1], buffer, 1);
} while (n == -1 && errno == EINTR);
exit(127);
}
close(datafd[1]);
}
if (pathname[0] == '/')
execv(pathname, argv);
else
execvp(pathname, argv);
buffer[0] = errno;
close(datafd[1]);
/* In case it exists, we return EACCES instead of ENOENT. */
if (buffer[0] == ENOENT)
if (access(pathname, R_OK) == 0)
buffer[0] = EACCES;
do {
n = write(controlfd[1], buffer, 1);
} while (n == -1 && errno == EINTR);
exit(127);
}
*pidptr = pid;
close(datafd[1]);
close(controlfd[1]);
do {
n = read(controlfd[0], buffer, 1);
} while (n == -1 && errno == EINTR);
if (n == -1) {
close(datafd[0]);
close(controlfd[0]);
kill(pid, SIGKILL);
do {
p = waitpid(pid, NULL, 0);
} while (p == (pid_t)-1 && errno == EINTR);
errno = EIO;
return NULL;
} else
if (n == 1) {
close(datafd[0]);
close(controlfd[0]);
do {
p = waitpid(pid, NULL, 0);
} while (p == (pid_t)-1 && errno == EINTR);
errno = (int)buffer[0];
return NULL;
}
close(controlfd[0]);
out = fdopen(datafd[0], "r");
if (!out) {
close(datafd[0]);
kill(pid, SIGKILL);
do {
p = waitpid(pid, NULL, 0);
} while (p == (pid_t)-1 && errno == EINTR);
errno = EIO;
return NULL;
}
errno = 0;
return out;
}
int main(int argc, char *argv[])
{
FILE *cmd;
pid_t pid;
int c;
unsigned long bytes = 0UL;
if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s command [ arguments ... ]\n", argv[0]);
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
cmd = exec_subproc(&pid, argv[1], argv + 1);
if (!cmd) {
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
while ((c = getc(cmd)) != EOF) {
bytes++;
putchar(c);
}
fflush(stdout);
fclose(cmd);
c = reap_subproc(pid);
if (errno) {
fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
return EXIT_FAILURE;
}
if (WIFEXITED(c) && !WEXITSTATUS(c)) {
fprintf(stderr, "%s: %lu bytes; success.\n", argv[1], bytes);
return 0;
}
if (WIFEXITED(c)) {
fprintf(stderr, "%s: %lu bytes; failed (exit status %d)\n", argv[1], bytes, WEXITSTATUS(c));
return WEXITSTATUS(c);
}
if (WIFSIGNALED(c)) {
fprintf(stderr, "%s: %lu bytes; killed by signal %d (%s)\n", argv[1], bytes, WTERMSIG(c), strsignal(WTERMSIG(c)));
return 128 + WTERMSIG(c);
}
fprintf(stderr, "%s: %lu bytes; child lost.\n", argv[1], bytes);
return EXIT_FAILURE;
}
Compile using e.g.
gcc -Wall -Wextra -O2 example.c -o example
and run e.g.
./example date
./example ./example date -u
./example /bin/sh -c 'date | tr A-Za-z a-zA-Z'