2

I have the following code:

/**
 * Executes a program and waits for it to finish, taking pipes into account.
 * @param string $cmd Command line to execute, including any arguments.
 * @param string $input Data for standard input.
 * @param integer $timeout How much to wait from program in msecs (-1 to wait indefinitely).
 * @return array Array of "stdout", "stderr" and "return".
 */
function execute($cmd,$stdin=null,$timeout=-1){
    $proc=proc_open(
        $cmd,
        array(array('pipe','r'),array('pipe','w'),array('pipe','w')),
        $pipes=null
    );
    fwrite($pipes[0],$stdin);                  fclose($pipes[0]);
    $stdout=stream_get_contents($pipes[1]);    fclose($pipes[1]);
    $stderr=stream_get_contents($pipes[2]);    fclose($pipes[2]);
    $return=proc_close($proc);
    return array(
        'stdout' => $stdout,
        'stderr' => $stderr,
        'return' => $return
    );
}

It has two "problems".

  • The code is synchronous; it freezes until the target process closes.
  • So far, I've not been able to it from "freezing" without issuing a different kind of command (such as $cmd > /dev/null & on linux and start /B $cmd on windows)

I don't mind the "freeze", at all. I just need to implement that timeout.

Note: It is important that the solution is cross-platform compatible. It is also important that the $cmd doesn't have to change - I'm running some complex commands and I'm afraid there may be some issues, however, this depends on the type of fix - I'm happy to hear these out, just that I'd prefer a different alternative.

I've found some resources that may help:

3 Answers 3

2

There is a few mistakes on the code.

That is actually working:

function execute($cmd, $stdin = null, $timeout = -1)
{
    $proc=proc_open(
        $cmd,
        array(array('pipe','r'), array('pipe','w'), array('pipe','w')),
        $pipes
    );
    var_dump($pipes);
    if (isset($stdin))
    {
        fwrite($pipes[0],$stdin);
    }
    fclose($pipes[0]);

    stream_set_timeout($pipes[1], 0);
    stream_set_timeout($pipes[2], 0);

    $stdout = '';

    $start = microtime();

    while ($data = fread($pipes[1], 4096))
    {
        $meta = stream_get_meta_data($pipes[1]);
        if (microtime()-$start>$timeout) break;
        if ($meta['timed_out']) continue;
        $stdout .= $data;
    }

    $stdout .= stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);
    $return = proc_close($proc);

    return array(
        'stdout' => $stdout,
        'stderr' => $stderr,
        'return' => $return
    );
}
Sign up to request clarification or add additional context in comments.

1 Comment

If I run execute('php test.php', 3), and test.php sleeps for 30 seconds with no output, then this function blocks indefinitely.
1

Rather than stream_get_contents, you could look at using fread to gain more finely grained control over what your code is doing. That combined with stream_set_timeout may give you what you're looking for.

I tossed something together as a demonstration of what I was thinking might work - this code is completely untested and comes with no guarantees, but might send you in the right direction. ;)

function execute($cmd,$stdin=null,$timeout=-1){
    $proc=proc_open(
        $cmd,
        array(array('pipe','r'),array('pipe','w'),array('pipe','w')),
        $pipes=null
    );
    fwrite($pipes[0],$stdin);                  fclose($pipes[0]);

    stream_set_timeout($pipes[1], 0);
    stream_set_timeout($pipes[2], 0);

    $stdout = '';

    $start = microtime();

    while ($data = fread($pipes[1], 4096))
    {
        $meta = stream_get_meta_data($pipes[1]);
        if (microtime()-$start>$timeout) break;
        if ($meta['timed_out']) continue;
        $stdout .= $data;
    }

    $return = proc_close($proc);
    $stdout .= stream_get_contents($pipes[1]);
    $stderr = stream_get_contents($pipes[2]);

    return array(
        'stdout' => $stdout,
        'stderr' => $stderr,
        'return' => $return
    );
}

Comments

0

This seems to be working for me:

public function toPDF() {
    $doc = $this->getDocument();

    $descriptor = [
        ['pipe','r'],
        ['pipe','w'],
        ['file','/dev/null','w'], // STDERR
    ];
    $proc = proc_open('/usr/local/project/scripts/dompdf_cli.php',$descriptor,$pipes,sys_get_temp_dir());
    fwrite($pipes[0],"$doc[paper]\n$doc[html]");
    fclose($pipes[0]);

    $timeout = 30;

    stream_set_blocking($pipes[1], false);

    $pdf = '';

    $now = microtime(true);

    try {
        do {
            $elapsed = microtime(true) - $now;

            if($elapsed > $timeout) {
                throw new \Exception("PDF generation timed out after $timeout seconds");
            }
            $data = fread($pipes[1], 4096);
            if($data === false) {
                throw new \Exception("Read failed");
            }
            if(strlen($data) === 0) {
                usleep(50);
                continue;
            }
            $pdf .= $data;
        } while(!feof($pipes[1]));

        fclose($pipes[1]);
        $ret = proc_close($proc);
    } catch(\Exception $ex) {
        fclose($pipes[1]);
        proc_terminate($proc); // proc_close tends to hang if the process is timing out
        throw $ex;
    } 


    if($ret !== 0) {
        throw new \Exception("dompdf_cli returned non-zero exit status: $ret");
    }

    // dump('returning pdf');
    return $pdf;
}

I'm not sure what the purpose of stream_set_timeout is -- that just sets the per-read timeout, but if you want to limit the overall time, you just have to set the stream to non-blocking mode and then time how long it takes.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.