Interaction of subprocess and condition handling

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

Interaction of subprocess and condition handling

Lassi Kortela
How do you handle errors signaled by the 'process' procedure from the
(chicken process) module in Chicken 5?

When I try things like

   (receive (from-child to-child child)
       (condition-case
           (process command args)
         (_ ()
            (values #f #f #f)))
     ...)

I get weird results - I have some 'display' calls, and according to
'current-process-id' some of them are coming from a different PID (I
assume 'process' does fork/exec internally, so somehow some of the
display code actually ends up running in the forked child process.

I guess normally the unix execve() call succeeds so the child process
stops running the Chicken programs and executes the desired command. But
when execve() fails (e.g. due to a missing executable file) then the
child process continues running Chicken code instead of exiting?

_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users
Reply | Threaded
Open this post in threaded view
|

Re: Interaction of subprocess and condition handling

felix.winkelmann
> How do you handle errors signaled by the 'process' procedure from the
> (chicken process) module in Chicken 5?
>
> When I try things like
>
>    (receive (from-child to-child child)
>        (condition-case
>            (process command args)
>          (_ ()
>             (values #f #f #f)))
>      ...)
>
> I get weird results - I have some 'display' calls, and according to
> 'current-process-id' some of them are coming from a different PID (I
> assume 'process' does fork/exec internally, so somehow some of the
> display code actually ends up running in the forked child process.
>
> I guess normally the unix execve() call succeeds so the child process
> stops running the Chicken programs and executes the desired command. But
> when execve() fails (e.g. due to a missing executable file) then the
> child process continues running Chicken code instead of exiting?

*Normally*, closing both ports returned from "process" will do a
waitpid(2) (or the equivalent) and throw an exception if WIFEXITED
returns false.

Somehow I can't make this work - may this be broken?


felix


_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users
Reply | Threaded
Open this post in threaded view
|

Re: Interaction of subprocess and condition handling

Lassi Kortela
Thanks for helping out :)

> *Normally*, closing both ports returned from "process" will do a
> waitpid(2) (or the equivalent) and throw an exception if WIFEXITED
> returns false.

So WIFEXITED checks for a normal exit (instead of being terminated by
signal, e.g. segfault or kill -9). But what if the execve() itself fails
so the forked child process doesn't get the chance to replace the
Chicken process image with the other program that it was asked to start?

When coding in C, the forked child process is usually programmed to call
_exit() in case the execve() fails. But _exit() causes a normal exit of
the child process - it doesn't terminate abnormally by signal. This is
tricky for the parent process, which can't tell the difference between a
failed execve() and a successful execve() of another program that then
chooses to exit with a non-zero exit code.

I'm under the impression that a special exit code (arbitrarily chosen
magic number, often 126?) is usually reserved to indicate a failed
execve(). What does Chicken do - if execve() fails, will the lingering
Chicken child process call _exit() with a particular exit code to notify
the parent? My test suggests that the child will keep running Scheme
code instead of exiting. But I may have messed something up myself.

Sorry for not posting the verbatim source code of a program that
reproduces the bug. Here is one:

;;;; csc -R r7rs test.scm && ./test

(import (r7rs) (chicken process) (chicken process-context posix))

(define (show . xs)
   (for-each display xs)
   (newline))

(define (demo)
   (receive (from-child to-child child)
       (condition-case
           (process "does-not-exist-so-execve-will-fail" '())
         (_ ()
            (show "exception handler entered in pid " (current-process-id))
            (values #f #f #f)))
     (and child
          (let ((output (read-string #f from-child)))
            (close-port from-child)
            (close-port to-child)
            output))))

(show "subprocess result = " (demo))
(show "current-process-id = " (current-process-id))

_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users
Reply | Threaded
Open this post in threaded view
|

Re: Interaction of subprocess and condition handling

Lassi Kortela
OK, I perused the chicken-core git repo. posixunix.scm defines the
'process' procedure. It's a wrapper for the 'process-impl' procedure in
the same file. process-impl does a lot of setup, then:

   (chicken.process#process-fork
     (lambda ()
       ;; ...set up input and output pipes...
       (chicken.process#process-execute cmd args env)))

And chicken.process#process-execute is also defined in the same file. I
presume it's the execve() wrapper. The execve() error is handled in it
as follows:

    (when (fx= r -1)
      (posix-error #:process-error 'process-execute
                   "cannot execute process" filename))

If 'process-execute' is run from within 'process', then
'process-execute' is running in the forked child process, and the above
error is also signaled in that child process instead of the parent.

Since the code that caused the bug for me had my own 'condition-case'
exception handler wrapped around the 'process' call, it would catch an
exception in the child process, not in the parent.

And since normal control flow proceeds after a 'condition-case'
exception handler has run, the forked child would continue running my
Scheme program concurrently with the parent!

Probably you should change the implementation of 'process' so it calls a
version of 'process-execute' that's specialized to run inside a child
process forked with the sole purpose of "execve() or die trying" :)

_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users
Reply | Threaded
Open this post in threaded view
|

Re: Interaction of subprocess and condition handling

Lassi Kortela
I think you'd basically have to replicate the following C traditional
programming technique in whatever mixture of Scheme and C is appropriate
for Chicken. So when you run "./spawn date" it succeeds and prints
output from the standard Unix "date" utility. But when you run "./spawn
nonexistent" it writes an error message to stderr (the child process
does the writing since it has access to the precise errno value after
execve() whereas the parent does not, so the child can show a more
precise error message). And the exit code from the parent is the nonzero
code 1 to indicate error.

Obviously in Chicken you would not exit the parent, but you'd exit the
child with code 126 as here. In Chicken you'd probably want some fancy
way to propagate the errno value from the child to the parent after a
failed execve(). It's unwise to do something like _exit(errno); One
solution would be to open a close-on-exec pipe from parent to child at a
known fd number and pass the errno value via the pipe. The parent would
probably have to poll() or select() on the pipe to check for error, but
I haven't thought this through.

Source code, also downloadable at <https://misc.lassi.io/2019/spawn.c>:

----------------------------------------------------------------------

// The child process uses the magic exit code 126 to indicate to the
// parent process that execve() failed. You could use another code,
// but IIRC this is the traditional choice.

#include <sys/wait.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void warn(const char *msg) {
     fprintf(stderr, "%s\n", msg);
}

static void warnsys(const char *msg) {
     fprintf(stderr, "%s: %s\n", msg, strerror(errno));
}

static void diesys(const char *msg) {
     warnsys(msg);
     exit(1);
}

static void die(const char *msg) {
     warn(msg);
     exit(1);
}

static void spawn(char **argv) {
     pid_t child;
     int status;

     if ((child = fork()) == -1) {
         diesys("cannot fork");
     }
     if (!child) {
         // We are in the new child process.
         execvp(argv[0], argv);
         warnsys("cannot exec child process");
         _exit(126);
     }
     // We are in the old parent process.
     if (waitpid(child, &status, 0) == -1) {
         diesys("cannot wait for child process");
     }
     if (!WIFEXITED(status)) {
         die("child process did not exit normally");
     }
     switch (WEXITSTATUS(status)) {
     case 0:
         // Success, all good.
         break;
     case 126:
         // Exec failed. Child already printed error message. Since
         // this we are the parent, we stay silent -- there's no need
         // to write a duplicate error message. But propagate the error
         // to the process that started _us_ by exiting with a nonzero
         // code anyway.
         exit(1);
     default:
         // Some other error.
         fprintf(stderr, "Child exited with code %d\n",
                 (int)WEXITSTATUS(status));
         break;
     }
}

int main(int argc, char **argv) {
     if (argc < 2) {
         die("usage: spawn command [args...]");
     }
     spawn(&argv[1]);
     return 0;
}


_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users
Reply | Threaded
Open this post in threaded view
|

Re: Interaction of subprocess and condition handling

Lassi Kortela
> Obviously in Chicken you would not exit the parent, but you'd exit the
> child with code 126 as here. In Chicken you'd probably want some fancy
> way to propagate the errno value from the child to the parent after a
> failed execve(). It's unwise to do something like _exit(errno); One
> solution would be to open a close-on-exec pipe from parent to child at a
> known fd number and pass the errno value via the pipe. The parent would
> probably have to poll() or select() on the pipe to check for error, but
> I haven't thought this through.
I tried that technique and it worked surprisingly easily! Source code,
below, also downloadable at <https://misc.lassi.io/2019/exec-error-pipe.c>.

Beware that threads and signals may require extra caution! This test
program only has one thread and I didn't think about signals at all. I
also don't handle EINTR.

----------------------------------------------------------------------

#include <sys/wait.h>

#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static void warn(const char *msg) {
     fprintf(stderr, "%s\n", msg);
}

static void warnsys(const char *msg) {
     fprintf(stderr, "%s: %s\n", msg, strerror(errno));
}

static void diesys(const char *msg) {
     warnsys(msg);
     exit(1);
}

static void die(const char *msg) {
     warn(msg);
     exit(1);
}

static void
set_close_on_exec(int fd)
{
     int flags;

     if ((flags = fcntl(fd, F_GETFD)) == -1) {
         diesys("cannot get file descriptor flags");
     }
     flags &= ~FD_CLOEXEC;
     if (fcntl(fd, F_SETFD, flags) == -1) {
         diesys("cannot set close-on-exec");
     }
}

static void spawn(char **argv) {
     pid_t child;
     int status, exec_errno;
     int fds[2];
     struct pollfd pollfd;
     ssize_t nbyte;

     if (pipe(fds) == -1) {
         diesys("cannot create pipe");
     }
     set_close_on_exec(fds[0]);
     set_close_on_exec(fds[1]);
     if ((child = fork()) == -1) {
         diesys("cannot fork");
     }
     if (!child) {
         // We are in the new child process.
         close(fds[0]);
         execvp(argv[0], argv);
         exec_errno = errno;
         nbyte = write(fds[1], &exec_errno, sizeof(exec_errno));
         if (nbyte == (ssize_t)-1) {
             warnsys("completely borked (child)");
         } else if (nbyte != (ssize_t)sizeof(exec_errno)) {
             warn("completely borked (child)");
         }
         _exit(126);
     }
     // We are in the old parent process.
     close(fds[1]);
     memset(&pollfd, 0, sizeof(pollfd));
     pollfd.fd = fds[0];
     pollfd.events = POLLIN;
     if (poll(&pollfd, 1, -1) == -1) {
         diesys("cannot poll");
     }
     exec_errno = 0;
     if (pollfd.revents & POLLIN) {
         nbyte = read(pollfd.fd, &exec_errno, sizeof(exec_errno));
         if (nbyte == 0) {
             // We don't get any data, means the pipe was closed.
         } else if (nbyte == (ssize_t)-1) {
             warnsys("completely borked (parent)");
         } else if (nbyte != (ssize_t)sizeof(exec_errno)) {
             warn("completely borked (parent)");
         }
     }
     if (waitpid(child, &status, 0) == -1) {
         diesys("cannot wait for child process");
     }
     if (!WIFEXITED(status)) {
         die("child process did not exit normally");
     }
     if (exec_errno != 0) {
         errno = exec_errno;
         warnsys("cannot execute command");
     } else if (WEXITSTATUS(status) == 0) {
         // Success, all good.
     } else {
         fprintf(stderr, "Child exited with code %d\n",
                 (int)WEXITSTATUS(status));
     }
}

int main(int argc, char **argv) {
     if (argc < 2) {
         die("usage: spawn command [args...]");
     }
     spawn(&argv[1]);
     return 0;
}

_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users
Reply | Threaded
Open this post in threaded view
|

Re: Interaction of subprocess and condition handling

Lassi Kortela
Sorry, that code cleared the FD_CLOEXEC flag instead of setting it.
Fixed code at  <https://misc.lassi.io/2019/exec-error-pipe.c>.

_______________________________________________
Chicken-users mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/chicken-users