5

I need to echo text to a named pipe (FIFO) in Linux. Even though I'm running in background with '&' and redirecting all output to a /dev/null, the shell_exec call always blocks.

There are tons of answers to pretty much exactly this question all over the internet, and they all basically point to the following php manual section:

If a program is started with this function, in order for it to continue running in the background, the output of the program must be redirected to a file or another output stream. Failing to do so will cause PHP to hang until the execution of the program ends.

And sure enough, when I try the non-blocking approach (of backgrounding and redirecting to /dev/null) with other commands like sleep, php successfully executes without hanging. But for the case of echo-ing to the FIFO, php hangs even though running the same command with bash produces no visible output and immediately returns to the shell.

In bash, I can run:

bash$ { echo yay > fifo & } &> /dev/null
bash$ cat fifo
yay
[1]+  Done                    echo yay > fifo

but when running the following php file with php echo.php:

<?php
shell_exec("{ echo yay > fifo & } &> /dev/null");
?>

it hangs, unless I first open fifo for reading.

So my question is, why is this blocking, but sleep isn't? In addition, I want to know what is happening behind the scenes: when I put the '&' in the php call, even though the shell_exec call blocks, the echo call clearly doesn't block whatever bash session php invoked it on, because when I CTRL+C out of php, I can read 'yay' from the FIFO (if I don't background the echo command, after CTRL+C the FIFO contains no text). This suggests that perhaps php is waiting on the pid of the echo command before going to the next instruction. Is this true?

Carlos Vazquez
  • 426
  • 3
  • 9

1 Answers1

2

I've been trying something similar and in the end came up with this solution:

/**
 * This runs a shell command on the server under the current PHP user, that is in CLI mode it is the user you are logged in with.
 * If a command is run in the background the method will return the location of the tempfile that captures the output. In that case you will have to manually remove the temporary file.
 */
static public function command($cmd, $show_output = true, $escape_command = false, $run_in_background = false)   
{ 
  if ($escape_command)
    $cmd = escapeshellcmd($cmd);

  $f = trim(`mktemp`);
  passthru($cmd . ($show_output ? " | tee $f" : " > $f") . ($run_in_background ? ' &' : '')); 
  return $run_in_background ? $f : trim(`cat $f ; rm -rf $f`);
}

The trick is to write the output to a temporary file and return that when the command has finished (blocking behavior) or just return the file path (non-blocking behavior). Also, I'm using passthru rather than shell_exec because interactive sessions are not possible with the latter because of the blocking behavior.

Tox
  • 373
  • 4
  • 13