Search code examples
symfonycommand-lineprocesssignalssymfony-process

Signal being forwarded to children for the symfony process component


I'm trying to write a small script that will manage a series of background processes using the symfony component Process (http://symfony.com/doc/current/components/process.html).

For this to work correctly i would like to handle signals sent to the main process, mainly SIGINT (ctrl + c).

When the main process gets this signal, it should stop starting new processes, wait for all current processes to exit and then exit itself.

I successfully catch the signal in the main process but the problem is that the child-processes gets the signal too and exits immediately.

Is there any way of changing this behavior or interrupting this signal?

This is my example script to demonstrate the behavior.

#!/usr/bin/env php
<?php
require_once __DIR__ . "/vendor/autoload.php";
use Symfony\Component\Process\Process;

$process = new Process("sleep 10");
$process->start();

$exitHandler = function ($signo) use ($process) {
    print "Got signal {$signo}\n";
    while ($process->isRunning()) {
        usleep(10000);
    }
    exit;
};
pcntl_signal(SIGINT, $exitHandler);

while (true) {
    pcntl_signal_dispatch();
    sleep(1);
}

Running this script, and sending the signal (pressing ctrl + c) will immediately stop the parent and child processes).

If i replace the while-loop with the isRunning call and the sleep with a call to the wait-method on the process i get an RuntimeException saying: The process has been signaled with signal "2".

If i take a more manual approach and execute the child process with phps build in exec, i get the behavior i want.

#!/usr/bin/env php

<?php
require_once __DIR__ . "/vendor/autoload.php";

exec(sprintf("%s > %s 2>&1 & echo $! >> %s", "sleep 10", "/dev/null", "/tmp/testscript.pid"));

$exitHandler = function ($signo) {
    print "Got signal {$signo}\n";
    $pid = file_get_contents("/tmp/testscript.pid");
    while (isRunning($pid)) {
        usleep(10000);
    }
    exit;
};
pcntl_signal(SIGINT, $exitHandler);

while (true) {
    pcntl_signal_dispatch();
    sleep(1);
}

function isRunning($pid){
    try{
        $result = shell_exec(sprintf("ps %d", $pid));
        if( count(preg_split("/\n/", $result)) > 2){
            return true;
        }
    }catch(Exception $e){}

    return false;
}

In this case, when i send the signal, the main process waits for it's child to finish before exiting.

Is there any way to get the behavior in the symfony process component?


Solution

  • It's not the behavior of Symfony's Process, but behavior of ctrl+c in UNIX terminal. When you press ctrl+c in terminal signal is sent to process group (parent and all child processes).

    Manual approach works because sleep isn't child process. When you want to use Symfony's component you can change child's process group with posix_setpgid:

    $otherGroup = posix_getpgid(posix_getppid());
    posix_setpgid($process->getPid(), $otherGroup);
    

    Then signal won't be sent to $process. That's the only working solution I found when I recently tackled with similar problem.

    Research

    Sending signals to process group

    Child process is created in Symfony example. You can check it in terminal.

    # find pid of your script
    ps -aux | grep "myscript.php"
    
    # show process tree
    pstree -a pid
    # you will see that sleep is child process
    php myscript.php
      └─sh -c sleep 20
          └─sleep 20
    

    Signal sent to process group is nicely visible when you print information about process in $exitHandler:

    $exitHandler = function ($signo) use ($process) {
        print "Got signal {$signo}\n";
        while ($process->isRunning()) {
            usleep(10000);
        }
        $isSignaled = $process->hasBeenSignaled() ? 'YES' : 'NO';
        echo "Signaled? {$isSignaled}\n";
        echo "Exit code: {$process->getExitCode()}\n\n";
        exit;
    };
    

    When you press ctrl+c or kill process group:

    # kill process group (like in ctrl+c)
    kill -SIGINT -pid
    
    # $exitHandler's output
    Got signal 2
    Signaled? YES
    Exit code: 130
    

    When signal is send only to parent process then you'll get expected behavior:

    # kill only main process
    kill -SIGINT pid
    
    # $exitHandler's output
    Got signal 2
    Signaled? NO
    Exit code: 0
    

    Now the solution is obvious. Don't create child process or change processs group, so signal is sent only to parent process.

    Disadvantages of changing process group

    Be aware of consequences when real child process isn't used. $process won't be terminated when parent process is killed with SIGKILL. If $process is long-running script then you could have multiple running instances after restarting parent process. It's good idea to check running processes before starting $process.