4

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";
}
?>
JorgeeFG
  • 5,651
  • 12
  • 59
  • 92
  • Well your code contains an endless loop. Why would you expect it to not do that endless loop? Also: Which SAPI are you using here? What happens for example if you start that script in CLI and abort it with CTRL+C? Is the "child-process" killed? Does `proc_open` open a child process or an independent one? And what is the timeout with `fgets`? I mean what do you do if the STDOUT has nothing to be read from - how long would that wait? – hakre Oct 10 '13 at 06:15
  • @hakre I can confirm that it kills the inotifywait process with CTRL+C in console mode. fgets does not have timeout because I try checking `connection_aborted()` and `proc_terminate()` like Jon's answer but didn't help, which is strange, since it is documented that PHP will know that connection was aborted only after trying to output something (which I do). Also, without checking those functions, the script should terminate on cancelling the window load, don't you think? – JorgeeFG Oct 10 '13 at 12:07
  • What php version you use ? – Omiga Oct 13 '13 at 03:45

4 Answers4

1

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).

pozs
  • 34,608
  • 5
  • 57
  • 63
1

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)

Niko Sams
  • 4,304
  • 3
  • 25
  • 44
0

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;
    }
}
Jon
  • 428,835
  • 81
  • 738
  • 806
  • Thanks, does it work for you? I've tried every combination of what you told me but I had no luck, the process remains open. I even make a new file after I cancel my browser to generate an output and let PHP know that the connection was aborted, but no luck. – JorgeeFG Sep 16 '13 at 13:29
  • @Jorge: Actually it doesn't work for me either. Unfortunately I can't look into this right now, will try to get back to it later. – Jon Sep 16 '13 at 13:44
  • It is something strange, maybe it is stuck on the `$s = fgets($pipes[1])` because of an unknown (at least for me) language thing. – JorgeeFG Oct 07 '13 at 13:44
0

Does this help?

$proc_info = proc_get_status($process);
pcntl_waitpid($proc_info['pid']);
Alain Tiemblo
  • 36,099
  • 17
  • 121
  • 153
Lajos Veres
  • 13,595
  • 7
  • 43
  • 56