Перенаправить канал, полученный proc_open (), в файл на оставшуюся часть времени процесса

Скажем, в PHP, У меня есть куча юнит-тестов.
Скажем, они требуют, чтобы какая-то служба работала.

В идеале я хочу, чтобы мой скрипт начальной загрузки:

  • запустить эту услугу
  • ждать, пока служба достигнет желаемого состояния
  • ручное управление для выбора модульного тестирования для запуска тестов
  • убирать по окончании тестов, корректно завершая службу соответствующим образом
  • установить какой-либо способ захвата всех выходных данных из службы по пути регистрации и отладки

Я сейчас пользуюсь proc_open() чтобы инициализировать мой сервис, захватив выходные данные с помощью механизма конвейера, проверив, что сервис переходит в нужное мне состояние, изучив выходные данные.

Однако в этот момент я в замешательстве — как я могу перехватить остальную часть вывода (включая STDERR) на оставшуюся часть сценария, в то же время позволяя выполнять мои модульные тесты?

Я могу подумать о нескольких потенциально долгих решениях, но прежде чем тратить время на их исследование, я хотел бы узнать, сталкивался ли кто-нибудь еще с этой проблемой и какие решения они нашли, если таковые имеются, не влияя на ответ.

Редактировать:

Вот урезанная версия класса, который я инициализирую в своем скрипте начальной загрузки (с new ServiceRunner), для справки:

<?phpnamespace 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);
}
}

4

Решение

Предположим, что сервис реализован как service.sh скрипт оболочки со следующим содержимым:

#!/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'

Сценарий эмулирует процесс запуска, печатает сообщение, указывающее, что служба готова, и выводит некоторые выходные данные после запуска.

Поскольку вы не продолжаете модульные тесты до тех пор, пока не будет прочитан «маркер готовности к обслуживанию», я не вижу особой причины делать это асинхронно. Если вы хотите запустить какой-либо процесс (обновление пользовательского интерфейса и т. Д.) Во время ожидания службы, я бы предложил использовать расширение с асинхронными функциями (Pthreads, эв, событие так далее.).

Однако, если есть только две вещи, которые должны быть выполнены асинхронно, то почему бы не форкнуть процесс? Служба может работать в родительском процессе, а модульные тесты могут запускаться в дочернем процессе:

<?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
1

Другие решения

Вы можете использовать pcntl_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]
}

?>
0

То, что я в итоге сделал, дойдя до точки, когда служба была правильно инициализирована, состояло в том, чтобы перенаправить каналы из уже открытого процесса в качестве стандартного ввода в cat процесс на трубу, также открыт proc_open() (помогло этот ответ).

Это была не вся история, так как я дошел до этого момента и понял, что асинхронный процесс через некоторое время завис из-за переполнения буфера потока.

Ключевая часть, в которой я нуждался (предварительно установив потоки в неблокирующее состояние), состояла в том, чтобы перевести потоки в режим блокировки, чтобы буфер утек в принимающий 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 = []);
}
0
По вопросам рекламы [email protected]