JorgeeFG
JorgeeFG

Reputation: 5981

proc_open leaves zombie process

The following scripts monitors /dev/shm/test for new files and outputs info about it in real time.

The problem is that when user closes the browser, a inotifywait process remains open, and so on.

Is there any way to avoid this?

<?php
$descriptorspec = array(
  0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
  1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
  2 => array("pipe", "a") // stderr is a file to write to
);

$process = proc_open('inotifywait -mc -e create /dev/shm/test/', $descriptorspec, $pipes);

if (is_resource($process)) {

  header("Content-type: text/html;charset=utf-8;");
  ob_end_flush(); //ends the automatic ob started by PHP
  while ($s = fgets($pipes[1])) {
    print $s;
    flush();
  }
  fclose($pipes[1]);
  fclose($pipes[0]);
  fclose($pipes[2]);

  // It is important that you close any pipes before calling
  // proc_close in order to avoid a deadlock
  $return_value = proc_close($process);

  echo "command returned $return_value\n";
}
?>

Upvotes: 4

Views: 3239

Answers (4)

Niko Sams
Niko Sams

Reputation: 4414

As inotifywait runs as own process that basically never ends you need to send it a KILL signal. If you run the script on cli the Ctrl+C signal is sent to the inotifywait process too - but you don't have that when running in the webserver.

You send the signal in a function that gets called by register_shutdown_function or by __destruct in a class.

This simple wrapper around proc_open could help:

class Proc
{
    private $_process;
    private $_pipes;

    public function __construct($cmd, $descriptorspec, $cwd = null, $env = null)
    {
        $this->_process = proc_open($cmd, $descriptorspec, $this->_pipes, $cwd, $env);
        if (!is_resource($this->_process)) {
            throw new Exception("Command failed: $cmd");
        }
    }

    public function __destruct()
    {
        if ($this->isRunning()) {
            $this->terminate();
        }
    }

    public function pipe($nr)
    {
        return $this->_pipes[$nr];
    }

    public function terminate($signal = 15)
    {
        $ret = proc_terminate($this->_process, $signal);
        if (!$ret) {
            throw new Exception("terminate failed");
        }
    }

    public function close()
    {
        return proc_close($this->_process);
    }

    public function getStatus()
    {
        return proc_get_status($this->_process);
    }

    public function isRunning()
    {
        $st = $this->getStatus();
        return $st['running'];
    }
}

$descriptorspec = array(
    0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
    1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
    2 => array("pipe", "a") // stderr is a file to write to
);
$proc = new Proc('inotifywait -mc -e create /dev/shm/test/', $descriptorspec);

header("Content-type: text/html;charset=utf-8;");
ob_end_flush(); //ends the automatic ob started by PHP
$pipe = $proc->pipe(1);
while ($s = fgets($pipe)) {
    print $s;
    flush();
}
fclose($pipe);

$return_value = proc->close($process);

echo "command returned $return_value\n";

Or you could use the Symfony Process Component which does exactly the same (plus other useful things)

Upvotes: 1

pozs
pozs

Reputation: 36264

That's because inotifywait will wait until changes happen to the file /dev/shm/test/, then will output diagnostic information on standard error and event information on standard output, and fgets() will wait until it can read a line: Reading ends when $length - 1 bytes (2nd parameter) have been read, or a newline (which is included in the return value), or an EOF (whichever comes first). If no length is specified, it will keep reading from the stream until it reaches the end of the line.

So basically, you should read data from the child process' stdout pipe non-blocking mode with stream_set_blocking($pipes[1], 0), or check manually if there is data on that pipe with stream_select().

Also, you need to ignore user abort with ignore_user_abort(true).

Upvotes: 1

Lajos Veres
Lajos Veres

Reputation: 13725

Does this help?

$proc_info = proc_get_status($process);
pcntl_waitpid($proc_info['pid']);

Upvotes: 0

Jon
Jon

Reputation: 437664

You can use ignore_user_abort to specify that the script should not stop executing when the user closes the browser window. That will solve half of the problem, so you also need to check if the window was closed inside your loop with connection_aborted to determine when you need to shut down everything in an orderly manner:

header("Content-type: text/html;charset=utf-8;");
ignore_user_abort(true);
ob_end_flush(); //ends the automatic ob started by PHP
while ($s = fgets($pipes[1])) {
    print $s;
    flush();
    if (connection_aborted()) {
        proc_terminate($process);
        break;
    }
}

Upvotes: 0

Related Questions