proc_open hangs when trying to read from a stream
Asked Answered
L

1

6

I've encountered the issue with proc_open on Windows, when trying to convert a wmv file (to flv), using ffmpeg, however I suspect I'll encounter the same scenario whenever certain conditions occur.
Basically my code is as follows:

$descriptorspec = array
(
    array("pipe", "r"),
    array("pipe", "w"),
    array("pipe", "w")
);

$pipes = array();
$procedure = proc_open('cd "C:/Program Files/ffmpeg/bin" && "ffmpeg.exe" -i "C:/wamp/www/project/Wildlife.wmv" -deinterlace -qdiff 2 -ar 22050 "C:/wamp/www/project/Wildlife.flv"', $descriptorspec, $pipes);
var_dump(stream_get_contents($pipes[1]));

Now, this code will cause PHP to hang indefinitely (it doesn't matter if instead of stream_get_contents I'll use fgets or stream_select, the behavior is consistent).

The reason for it (I suspect) is that, while STDOUT stream is open succesfully, the process doesn't write anything to it (even though running the same command in cmd displays output) and as such, trying to read from such stream, would cause the same issue as described here, so - PHP waits for the stream to have anything in it, process doesn't write anything to it.

However (additional fun), setting stream_set_timeout or stream_set_blocking doesn't have any effect.

As such - can somebody confirm/deny on what is going on, and, if possible, show how can I cater for such situation? I've looked at PHP bugs, and all proc_open hangs ones seem to be fixed.

For time being I've implemented such solution:

$timeout = 60;
while (true) {
    sleep(1);

    $status = proc_get_status($procedure);
    if (!$status['running'] || $timeout == 0) break;

    $timeout--;
}

However, I'd really not like to rely on something like this as:

  1. I will have processes that run for longer than a minute - such processes will be falsely reported to be of the above mentioned type
  2. I want to know when the ffmpeg has finished converting the video - currently I'll only know that process is still running after a minute, and I can't really do anything to check if there's any output (as it will hang PHP).

Also, I don't really want to wait a full minute for the process to be checked (for example - converting the given video from command line takes <10s), and I'll have videos that take more time to be converted.


Per comment from @Sjon, here's stream_select I was using, which blocks due to same issue - STDOUT not being written to:

$descriptorspec = array
(
    array("pipe", "r"),
    array("pipe", "w"),
    array("pipe", "w")
);

$pipes = array();
$procedure = proc_open('cd "C:/Program Files/ffmpeg/bin" && "ffmpeg.exe" -i "C:/wamp/www/sandbox/Wildlife.wmv" -deinterlace -qdiff 2 -ar 22050 "C:/wamp/www/sandbox/Wildlife.flv"', $descriptorspec, $pipes);

$read = array($pipes[0]);
$write = array($pipes[1], $pipes[2]);
$except = array();

while(true)
if(($num_changed_streams = stream_select($read, $write, $except, 10)) !== false)
{
    foreach($write as $stream)
        var_dump(stream_get_contents($stream));

    exit;
}
else
    break;

Per conversation with @Sjon - reading from buffered streams on Windows is broken. The solution in the end is to use stream redirection via shell, and then read the created files - as such

$descriptorspec = array
(
    array("pipe", "r"),
    array("pipe", "w"),
    array("pipe", "w")
);

$pipes = array();
$procedure = proc_open('cd "C:/Program Files/ffmpeg/bin" && "ffmpeg.exe" -i "C:/wamp/www/sandbox/Wildlife.mp4" -deinterlace -qdiff 2 -ar 22050 "C:/wamp/www/sandbox/Wildlife.flv" > C:/stdout.log 2> C:/stderr.log', $descriptorspec, $pipes);

proc_close($procedure);

$output = file_get_contents("C:/stdout.log");
$error = file_get_contents("C:/stderr.log");

unlink("C:/stdout.log");
unlink("C:/stderr.log");

As the stream is buffered, in the file we will get unbuffered output (something I was after as well). And we don't need to check if the file changes, because the result from shell is unbuffered and synchronous.

Lodmilla answered 2/7, 2015 at 20:33 Comment(3)
One thing to check: if your destination file already exists ffmpeg will ask you to confirm the overwrite and will wait indefinitely. If you want to overwrite without prompt put an -y before the output file path.Kola
No - the file doesn't exists.Lodmilla
I was having a similar issue, but only because I was making a file write with file_put_contents and a db->insert inside of my while (!feof($pipes[1])) {. However, if I removed one then my script completed. It didn't matter which one. So I might be using a php version with a bug, but it seems if there is too much file io going on it might conflict with the stream reading from the pipe and hangs.Yerkovich
S
3

This took some time to reproduce, but I found your problem. The command you run, outputs some diagnostics when you run it; but it doesn't output to stdout, but to stderr. The reason for this is explained in man stderr:

Under normal circumstances every UNIX program has three streams opened for it when it starts up, one for input, one for output, and one for printing diagnostic or error messages

If you would properly use streams; this wouldn't be an issue; but you call stream_get_contents($pipes[1]) instead. This results in PHP waiting for output from stdout, which never arrives. This fix is simple; read from stderr stream_get_contents($pipes[2]) instead and the script will quit immediately after the process ends

To expand on your addition of stream_select to the question; stream_select is not implemented on windows in php, it says so in the manual:

Use of stream_select() on file descriptors returned by proc_open() will fail and return FALSE under Windows.

So if the code posted above doesn't work; I'm not sure what will. Have you considered abandoning your streams solution, reverting to a simple exec()-call instead? If you append >%TEMP%/out.log 2>%TEMP%/err.log to your command you can still read output from the process and it might finish quicker (without waiting for the unmodifiable timeout)

Seema answered 7/7, 2015 at 10:53 Comment(9)
As I mentioned, doing it via stream_get_contents or stream_select has the same effect. I failed to mention that reading from STDERR has the same effect as well (can't read from any of the streams - it causes the function to hang), if I remember correctly (or it might be due to the fact I'm reading from STDOUT first, will see). Bear in mind that this is under Windows. Will update when I'm at home.Lodmilla
I have tested this pretty extensively, I'd be surprised if it doesn't fix your problemSeema
I've updated the question, with stream_select I was testing with, which suffers from the same issue, irregardless of which stream you read from. You were right however, in regards that the output from ffmpeg is sent to STDERR (when testing just with stream_get_contents($pipes[2]), and I misremembered regarding reading from it, or did something incorrectly; yes - the output for the command is sent to STDERR. This unfortunately makes things tricky, as I have a process, that succesfully ends, yet serves content within STDERR, which so far I've been using to gather information when [cont]Lodmilla
the process ends, well, erroneously. However, from what I'm seeing is that such approach is wrong, as it depends upon the process to what stream it writes to. On the other hand - this process DOES finish running (when run from console), and it does so in specific time (certainly less than my timeout) - so proc_get_status is now fishy.Lodmilla
Also - bear in mind this is a generic situation - the command that is passed doesn't matter. As such, I'll have content written either to STDOUT or STDERR - how can I determine from which stream should I read from? Reading from the one that's open, but doesn't have any content sent to results in PHP hanging, so which one should I choose? :DLodmilla
I've updated my answer; there are multiple issues with streams under Windows; see bugs.php.net/bug.php?id=34972, bugs.php.net/bug.php?id=47918 and bugs.php.net/bug.php?id=51800Seema
Well - then manual is wrong, because it certainly doesn't return false :) I've arrived to the same bug reports, but from the side of stream_set_timeout :D The approach with redirecting streams to files might be the thing I'm looking for - in the end I'll only have to create an interval to check if the "file buffer" is changing size. Still not the solution I'm liking, but there's at least something I can continue with.Lodmilla
Thanks for the bounty; I wish I could help you further but I do not have a Windows machine to perform additional testing with. It might be worth opening another bugreport asking for help from the php-devvers; esp since you have a nice reproducible problemSeema
No worries - you gave me an idea how to tackle this, so that amounts to something :) And seeing that some of these bugs have 10 years I don't think opening a report will amount to anything :DLodmilla

© 2022 - 2024 — McMap. All rights reserved.