0

I'm new to macOS dev. Most of my background is on Windows.

I am trying to write a function for my launch daemon that should run a Bash script via its file path and then get notified asynchronously when it finishes running and get its exit code (I think it's called "status code" on Linux.) Or send an error in the callback if it fails to run the Bash script.

I'm using the following, which also includes my two-part question in comments:

void RunExternalScript(const char* pScriptFilePath,
                       void (*pfnCallback)(int exitCode, const void* pParam),
                       const void* pParam)
{
    //'pfnCallback' = should be called when 'pScriptFilePath' finishes running

    //Fork our process
    pid_t pidChild = fork();
    if(pidChild == 0)
    {
        //Child process, start our Bash script
        execl(pScriptFilePath, (const char*)nullptr);

        //We get here only if we failed to run our script
        log("Error %d running script: %s", errno, pScriptFilePath);

        abort();
    }
    else if(pidChild != -1)
    {
        //Started our script OK, but how do I wait for it
        //asynchronously to finish?
        //
        //If I call here:
        //
        //   int nExitCode;
        //   waitpid(pidChild, &nExitCode, 0);
        //
        //It will block until the child process exits, 
        //which I don't want to do since I may want to 
        //run more than one script, plus my daemon may 
        //receive a request to quit, so I need to be able
        //to cancel this wait for the child process...

        //And the second question:
        //
        //How do I know from this process that the forked
        //process failed to run my script?
    }
    else
    {
        log("Error running: %s", pScriptFilePath);
    }
}
273K
  • 29,503
  • 10
  • 41
  • 64
c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • Your problem is the `exec..` functions DO NOT RETURN. Upon successful execution the executing script/utility, becomes the process and has no way to relate back to what created it. Which is why you have `//We get here only if we failed to run our script`. Your desire for asynchronous discovery of the exit code does away with [man 2 wait](https://man7.org/linux/man-pages/man2/wait.2.html). You can have the script (or a wrapper for it) write the exit code to some PID file (or other chosen location), and then asynchronously check if the file exists, and if so, read the contents. – David C. Rankin Jan 08 '23 at 07:30
  • The "wrapper" being the script you call that then runs your current script writing `$?` out to the PID file after the script completes -- which your current code then checks for an reads when present. – David C. Rankin Jan 08 '23 at 07:35
  • AFAIK `fork+exec` on one end, handling of SIGCHILD+non-blocking `waitpid` on the other should do the trick see: https://stackoverflow.com/questions/7171722/how-can-i-handle-sigchld – alagner Jan 08 '23 at 08:49
  • @alagner: I implemented a callback for SIGCHILD via `kqueue` and `kevent` with `SIGCHLD, EVFILT_SIGNAL, EV_ADD | EV_RECEIPT` that would be processed via run-loop, but then when I call `waitpid(WNOHANG)` from it on the child PID, it works for one process. But if I invoke more than one of these at the same time (or close together in time) it creates some race condition when one call to `waitpid(WNOHANG)` returns 0, and I don't know how to address it. – c00000fd Jan 08 '23 at 09:55
  • It seems like processing of one `SIGCHLD` gets mangled and one notification is lost. Any idea what am I doing wrong? Or maybe someone can post a code sample to show? – c00000fd Jan 08 '23 at 09:56
  • Aren't you hitting [this](https://stackoverflow.com/a/48750947/4885321) problem? But basically: blending same closely-incomning signals into a single one seems a correct behaviour. BTW, isn't there a objC/Swift-specific call to do what you want? Perhaps it's way easier than doing all the bare POSIX-trickery. – alagner Jan 08 '23 at 10:03
  • 1
    You might find boost::process helpful https://www.boost.org/doc/libs/1_81_0/doc/html/boost_process/v2.html – Alan Birtles Jan 08 '23 at 10:25
  • @AlanBirtles: why would you want to bring the whole giant library just to do one small thing? This trend in modern development is so much beyond me. – c00000fd Jan 16 '23 at 08:27
  • @c00000fd doing it correctly, especially in s cross platform way isn't so small. It's much quicker to use an existing well tested library than spend time implementing and debugging something yourself – Alan Birtles Jan 16 '23 at 12:07
  • @AlanBirtles: I need it for macOS only. And tbh, it is very hard to write something other than a simple Hello World app in a cross platform way. – c00000fd Jan 16 '23 at 14:03

1 Answers1

1

You can use the "waitpid" function with the "WNOHANG" option. This will allow you to wait for the child process to finish, but not block the calling process. You can then also check the return value of "waitpid" to determine if the child process has finished or not.

To check if the child process failed to run the script, you can check the return value of "execl".

void RunExternalScript(const char* pScriptFilePath,
void (pfnCallback)(int exitCode, const void pParam),
const void* pParam)
{
pid_t pidChild = fork();
if(pidChild == 0)
{
    if (execl(pScriptFilePath, (const char*)nullptr) == -1)
    {
        log("Error %d running script: %s", errno, pScriptFilePath);
        exit(EXIT_FAILURE);
    }
}
else if(pidChild != -1)
{
    int nExitCode;
    pid_t ret = waitpid(pidChild, &nExitCode, WNOHANG);
    if (ret == -1)
    {
        log("Error %d waiting for child process", errno);
    }
    else if (ret == 0)
    {

    }
    else
    {
        if (WIFEXITED(nExitCode))
        {
            nExitCode = WEXITSTATUS(nExitCode);
        }
        else
        {
            nExitCode = -1;
        }
        pfnCallback(nExitCode, pParam);
    }
}
else
{
    log("Error running: %s", pScriptFilePath);
}
}
  • Thanks for the `WNOHANG` suggestion. It will require starting a worker thread, or using a thread-pool of my own to poll for it. But it's doable. Question about `WIFEXITED` though. I never get it to work if I specify a wrong script path in `pScriptFilePath` and if execl fails. What's the trick there? – c00000fd Jan 09 '23 at 22:21