2

When I execute this on my system:

FILE* pipe = popen("something_that_doesnt_exist", "r");
printf("pipe: %p\n", pipe);

I get a valid pointer for the pipe, despite that program not existing. I'd like to write a popen that can detect that and return a NULL pointer indicating launch failure. However, I'm not sure how to achieve that while keeping the /bin/sh call for interpretation. Does anyone know how I could check the return status of a call like:

execl("/bin/sh", "sh", "-c", "something_that_doesnt_exist");
gct
  • 14,100
  • 15
  • 68
  • 107
  • http://stackoverflow.com/questions/903864/how-to-exit-a-child-process-and-return-its-status-from-execvp – Paul Apr 21 '16 at 02:58
  • `pclose()` gives you the exit status you need. Is that not good enough? – John Zwinck Apr 21 '16 at 03:10
  • well that wouldn't work because I would want the status before deciding whether to close the pipe I think. – gct Apr 21 '16 at 03:26
  • First, don't use `sh -c` if you intend to call a command. You only need it if you use the POSIX shell features (like substitution or job control). Second, for retrieving the `exec*()` status from the child process, you can use an extra close-on-exec pipe, that will only be used in case the `exec*()` fails. My answer below shows this approach in detail. If you need `sh` shell behaviour, then you can wrap your `something_that_doesnt_exist` inside an error reporting shell script snippet instead, and you do not need my answer to do that, just POSIX shell scripting. – Nominal Animal Apr 22 '16 at 12:37

4 Answers4

1

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'
Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • Unfortunately I think I need the posix shell semantics for what I'm doing. I'm basically writing a library that provides a super-popen capability including named pipes that can have multiple readers. It seems that there's truly no good way to do this in linux, which is both surprising and disappointing. – gct Apr 22 '16 at 21:07
  • @gct: Don't be silly :) The problem is that "there's truly no good way to do this" because you have restricted yourself to semantics that do not allow a robust, useful solution to the underlying problem. For example, consider the Python [subprocess](https://docs.python.org/3/library/subprocess.html) module, which is intended to solve a subset of the underlying problem. If you want a popen-like facility with multiple pipes (or even sockets) interconnecting the processes (including the parent), use structures to describe each, not a string! Then you can easily get all error messages, too. – Nominal Animal Apr 23 '16 at 03:47
0
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
    int fd = open("/dev/null", O_WRONLY);

    /* backup */
    int f1 = dup(1);
    int f2 = dup(2);
    dup2(fd, 1);
    dup2(fd, 2);

    int res = system("something_that_doesnt_exist");

    /* recovery */
    dup2(f1, 1);
    dup2(f2, 2);

    FILE* pipe = popen("something_that_doesnt_exist", "r");
    if (res != 0 )
    {
        pipe = NULL;
    }

}

redirect stdout and stderr in order to avoid unexcepted output.

JellyWang
  • 51
  • 1
  • I like it, but unfortunately my commands are of the sort that are either long running or not idempotent, so I can't really run them twice... – gct Apr 21 '16 at 12:46
0

If popen() will successfully execute a command or not, you can not tell. This is because the process calls fork() first, so the pipe and child process will always be created. But if the execv() call following the fork() fails, the child will die and the parent will not be able to tell if this is caused by execv() failure or the command you wanted just completed without any output.

Stian Skjelstad
  • 2,277
  • 1
  • 9
  • 19
0

If your process has no other child process, maybe you can use waitpid

int stat;
File* fp = popen("something_that_doesnt_exist", "r");
waitpid(-1, &stat, 0);

then you can determine the value of stat, if popen succeed, stat = 0. Not very sure about this way, need someone to confirm

Sunson
  • 88
  • 1
  • 8
  • Unfortunately in this case, I'll have lots of children, and I can't afford to block and wait. – gct Apr 21 '16 at 11:21