将 proc_open() 获取的管道重定向到文件以处理剩余的过程持续时间

Redirect pipe acquired by proc_open() to file for remainder of process duration

说,在 PHP 中,我有一堆单元测试。 假设他们需要一些服务 运行ning.

理想情况下,我希望我的 bootstrap 脚本能够:

我目前正在使用 proc_open() 来初始化我的服务,使用管道机制捕获输出,通过检查输出来检查服务是否达到我需要的状态。

然而在这一点上我很困惑 - 我如何才能在脚本的剩余持续时间内捕获剩余的输出(包括 STDERR),同时仍然允许我的单元测试 运行?

我能想到一些可能冗长的解决方案,但在花时间调查它们之前,我想知道是否有其他人遇到过这个问题,他们找到了哪些解决方案(如果有的话),没有影响响应。

编辑:

这是我在bootstrap脚本中初始化的class的精简版(带new ServiceRunner),供参考:

<?php


namespace Tests;


class ServiceRunner
{
    /**
     * @var resource[]
     */
    private $servicePipes;

    /**
     * @var resource
     */
    private $serviceProc;

    /**
     * @var resource
     */
    private $temp;

    public function __construct()
    {
        // Open my log output buffer
        $this->temp = fopen('php://temp', 'r+');

        fputs(STDERR,"Launching Service.\n");
        $this->serviceProc      = proc_open('/path/to/service', [
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("pipe", "w"),
        ], $this->servicePipes);

        // Set the streams to non-blocking, so stream_select() works
        stream_set_blocking($this->servicePipes[1], false);
        stream_set_blocking($this->servicePipes[2], false);

        // Set up array of pipes to select on
        $readables = [$this->servicePipes[1], $this->servicePipes[2]);

        while(false !== ($streams = stream_select($read = $readables, $w = [], $e = [], 1))) {
            // Iterate over pipes that can be read from
            foreach($read as $stream) {
                // Fetch a line of input, and append to my output buffer
                if($line = stream_get_line($stream, 8192, "\n")) {
                    fputs($this->temp, $line."\n");
                }

                // Break out of both loops if the service has attained the desired state
                if(strstr($line, 'The Service is Listening' ) !== false) {
                    break 2;
                }

                // If the service has closed one of its output pipes, remove them from those we're selecting on
                if($line === false && feof($stream)) {
                    $readables = array_diff($readables, [$stream]);
                }
            }
        }

        /* SOLUTION REQUIRED SOLUTION REQUIRED SOLUTION REQUIRED SOLUTION REQUIRED */
        /* Set up the pipes to be redirected to $this->temp here */

        register_shutdown_function([$this, 'shutDown']);
    }

    public function shutDown()
    {
        fputs(STDERR,"Closing...\n");
        fclose($this->servicePipes[0]);
        proc_terminate($this->serviceProc, SIGINT);
        fclose($this->servicePipes[1]);
        fclose($this->servicePipes[2]);
        proc_close($this->serviceProc);
        fputs(STDERR,"Closed service\n");

        $logFile = fopen('log.txt', 'w');

        rewind($this->temp);
        stream_copy_to_stream($this->temp, $logFile);

        fclose($this->temp);
        fclose($logFile);
    }
}

假设服务实现为service.sh shell脚本,内容如下:

#!/bin/bash -
for i in {1..4} ; do
  printf 'Step %d\n' $i
  printf 'Step Error %d\n' $i >&2
  sleep 0.7
done

printf '%s\n' 'The service is listening'

for i in {1..4} ; do
  printf 'Output %d\n' $i
  printf 'Output Error %d\n' $i >&2
  sleep 0.2
done

echo 'Done'

脚本模拟启动过程,打印服务准备就绪的消息,并在启动后打印一些输出。

由于在读取 "service-ready marker" 之前您不会继续进行单元测试,因此我认为没有特别的理由异步执行此操作。如果你想在等待服务时运行一些进程(更新UI等),我建议使用一个具有异步功能的扩展(pthreadsev, event 等).

但是,如果只有两件事要异步完成,那为什么不fork一个进程呢?服务可以在父进程中运行,单元测试可以在子进程中启动:

<?php
$cmd = './service.sh';
$desc = [
  1 => [ 'pipe', 'w' ],
  2 => [ 'pipe', 'w' ],
];
$proc = proc_open($cmd, $desc, $pipes);
if (!is_resource($proc)) {
  die("Failed to open process for command $cmd");
}

$service_ready_marker = 'The service is listening';
$got_service_ready_marker = false;

// Wait until service is ready
for (;;) {
  $output_line = stream_get_line($pipes[1], PHP_INT_MAX, PHP_EOL);
  echo "Read line: $output_line\n";
  if ($output_line === false) {
    break;
  }
  if ($output_line == $service_ready_marker) {
    $got_service_ready_marker = true;
    break;
  }

  if ($error_line = stream_get_line($pipes[2], PHP_INT_MAX, PHP_EOL)) {
    $startup_errors []= $error_line;
  }
}

if (!empty($startup_errors)) {
  fprintf(STDERR, "Startup Errors: <<<\n%s\n>>>\n", implode(PHP_EOL, $startup_errors));
}

if ($got_service_ready_marker) {
  echo "Got service ready marker\n";
  $pid = pcntl_fork();
  if ($pid == -1) {
    fprintf(STDERR, "failed to fork a process\n");

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);
  } elseif ($pid) {
    // parent process

    // capture the output from the service
    $output = stream_get_contents($pipes[1]);
    $errors = stream_get_contents($pipes[2]);

    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    // Use the captured output
    if ($output) {
      file_put_contents('/tmp/service.output', $output);
    }
    if ($errors) {
      file_put_contents('/tmp/service.errors', $errors);
    }

    echo "Parent: waiting for child processes to finish...\n";
    pcntl_wait($status);
    echo "Parent: done\n";
  } else {
    // child process

    // Cleanup
    fclose($pipes[1]);
    fclose($pipes[2]);
    proc_close($proc);

    // Run unit tests
    echo "Child: running unit tests...\n";
    usleep(5e6);
    echo "Child: done\n";
  }
}

示例输出

Read line: Step 1
Read line: Step 2
Read line: Step 3
Read line: Step 4
Read line: The service is listening
Startup Errors: <<<
Step Error 1
Step Error 2
Step Error 3
Step Error 4
>>>
Got service ready marker
Child: running unit tests...
Parent: waiting for child processes to finish...
Child: done
Parent: done

您可以使用 pcntl_fork() 命令 fork 当前进程来完成这两个任务并等待测试完成:

 <?php
 // [launch service here]
 $pid = pcntl_fork();
 if ($pid == -1) {
      die('error');
 } else if ($pid) {
      // [read output here]
      // then wait for the unit tests to end (see below)
      pcntl_wait($status);
      // [gracefully finishing service]
 } else {
      // [unit tests here]
 }

 ?>

我最终做的是,达到服务已正确初始化的地步,将管道从已打开的进程作为标准输入重定向到每个管道的 cat 进程,由 proc_open() 打开(由 this answer 帮助)。

这还不是全部,因为我到了这一步并意识到异步进程在一段时间后由于流缓冲区已满而挂起。

我需要的关键部分(之前已将流设置为非阻塞)是将流恢复为阻塞模式,以便缓冲区正确地排入接收 cat 进程。

完成我的问题的代码:

// Iterate over the streams that are stil open
foreach(array_reverse($readables) as $stream) {
    // Revert the blocking mode
    stream_set_blocking($stream, true);
    $cmd = 'cat';

    // Receive input from an output stream for the previous process,
    // Send output into the internal unified output buffer
    $pipes = [
        0 => $stream,
        1 => $this->temp,
        2 => array("file", "/dev/null", 'w'),
    ];

    // Launch the process
    $this->cats[] = proc_open($cmd, $pipes, $outputPipes = []);
}