I have a function that forks a process, duplicates file descriptors for input and output buffers, and then runs execl
on a command passed in via a string called cmd
:
static pid_t
c2b_popen4(const char* cmd, int pin[2], int pout[2], int perr[2], int flags)
{
pid_t ret = fork();
if (ret < 0) {
fprintf(stderr, "fork() failed!\n");
return ret;
}
else if (ret == 0) {
/*
Assign file descriptors to child pipes (not shown)...
*/
execl("/bin/sh", "/bin/sh", "-c", cmd, NULL);
fprintf(stderr, "execl() failed!\n");
exit(EXIT_FAILURE);
}
else {
/*
Close parent read and write pipes (not shown)...
*/
return ret;
}
return ret;
}
Each of the cmd
instances process my data correctly, so long as my test inputs are correct.
When bad data is passed to a child process, my parent program will run to completion and exit with a non-error status code of 0.
If I deliberately put in bad input — to purposefully try to get one of the cmd
instances to fail in an expected way — I'd like to know how to capture the exit status of that cmd
so that I can issue the correct error status code from the parent program, before termination.
How is this generally done?
You can get the exit status of the child via the first argument of wait()
, or the second argument of waitpid()
, and then using the macros WIFEXITED
and WEXITSTATUS
with it.
For instance:
pid_t ret = c2b_popen4("myprog", pin, pout, perr, 0);
if ( ret > 0 ) {
int status;
if ( waitpid(ret, &status, 0) == -1 ) {
perror("waitpid() failed");
exit(EXIT_FAILURE);
}
if ( WIFEXITED(status) ) {
int es = WEXITSTATUS(status);
printf("Exit status was %d\n", es);
}
}
A simplified working example:
failprog.c
:
int main(void) {
return 53;
}
shellex.c
:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
pid_t p = fork();
if ( p == -1 ) {
perror("fork failed");
return EXIT_FAILURE;
}
else if ( p == 0 ) {
execl("/bin/sh", "bin/sh", "-c", "./failprog", "NULL");
return EXIT_FAILURE;
}
int status;
if ( waitpid(p, &status, 0) == -1 ) {
perror("waitpid failed");
return EXIT_FAILURE;
}
if ( WIFEXITED(status) ) {
const int es = WEXITSTATUS(status);
printf("exit status was %d\n", es);
}
return EXIT_SUCCESS;
}
Output:
paul@thoth:~/src/sandbox$ ./shellex
exit status was 53
paul@thoth:~/src/sandbox$
waitpid()
will block until the process with the supplied process ID exits. Since you're calling your function with a popen()
name and passing pipes to it, presumably your child process doesn't terminate quickly, so that probably wouldn't be the right place to check it, if the call succeeded. You can pass WNOHANG
as the third parameter to waitpid()
to check if the process has terminated, and to return 0
if the child has not yet exited, but you have to be careful about when you do this, since you get no guarantees about which process will run when. If you call waitpid()
with WNOHANG
immediately after returning from c2b_popen4()
, it may return 0
before your child process has had a chance to execute and terminate with an error code, and make it look as if the execution was successful when it's just about to not be successful.
If the process does die immediately, you'll have problems reading from and writing to your pipes, so one option would be to check waitpid()
if you get an error from the first attempt to do that, to check if the read()
or write()
is failing because your child process died. If that turns out to be true, you can retrieve the exit status and exit your overall program then.
There are other possible strategies, including catching the SIGCHLD signal, since that'll be raised whenever one of your child processes dies. It would be OK, for instance, to call _exit()
right from your signal handler, after waiting for the child process (calling waitpid()
in a signal handler is also safe) and getting its exit status.