Компонент Process

Компонент Process

Компонент Process выполняет команды в подпроцессах.

Установка

1
$ composer require symfony/process

Также вы можете клонировать репозиторий https://github.com/symfony/process.

Note

Если вы устанавливаете этот компонент вне приложения Symfony, вам нужно подключить файл vendor/autoload.php в вашем коде для включения механизма автозагрузки классов, предоставляемых Composer. Детальнее читайте в этой статье.

Использование

Класс Process выполняет команду в подпроцессе, заботясь о разнице между ОС и экранированием аргументов, чтобы избежать проблем безопасности. Он заменяет PHP функции вроде exec, passthru, shell_exec и system:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

$process = new Process('ls -lsa');
$process->run();

// выполняет после окончания команды
if (!$process->isSuccessful()) {
    throw new ProcessFailedException($process);
}

echo $process->getOutput();

Tip

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

1
2
3
4
5
6
// команды, основанные на традиционных строках
$builder = new Process('ls -lsa');
// тот же пример, но с использованием массива
$builder = new Process(array('ls', '-lsa'));
// массив может содержать любое количество аргументов и опций
$builder = new Process(array('ls', '-l', '-s', '-a'));

Метод getOutput() всегда возвращает все содержимое стандартного вывода команды и содержимое getErrorOutput() вывода ошибки. Как вариант, методы getIncrementalOutput() и getIncrementalErrorOutput() возваращают новый вывод после последнего вызова.

Метод clearOutput() очищает содержание вывода, а clearErrorOutput() - содержание вывода ошибки.

Вы можете также использовать класс Process с концепцией foreach, чтобы получить вывод во время его генерации. По умолчанию, цикл ждёт нового вывода до перехода к следующей итерации:

1
2
3
4
5
6
7
8
9
10
$process = new Process('ls -lsa');
$process->start();

foreach ($process as $type => $data) {
    if ($process::OUT === $type) {
        echo "\nRead from stdout: ".$data;
    } else { // $process::ERR === $type
        echo "\nRead from stderr: ".$data;
    }
}

Tip

Компонент Процесс внутренне использует PHP итератор, чтобы получить вывод во время его генерации. Итератор демонстрируется через метод getIterator(), чтобы позволить настройку его поведения:

1
2
3
4
5
6
$process = new Process('ls -lsa');
$process->start();
$iterator = $process->getIterator($process::ITER_SKIP_ERR | $process::ITER_KEEP_OUTPUT);
foreach ($iterator as $data) {
    echo $data."\n";
}

Метод mustRun() идентичен методу run(), кроме того, что он будет вызывать ProcessFailedException, если процесс не мог быть выполнен успешно (т.е. процесс завершился ненулевым кодом):

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');

try {
    $process->mustRun();

    echo $process->getOutput();
} catch (ProcessFailedException $exception) {
    echo $exception->getMessage();
}

Получение вывода процесса в настоящем времени

При выполнении долгосрочной команды (вроде синхронизации файлов с удалённым сервером), вы можете предоставить обратную связь с конечным пользователем в реальном времени, передав анонимную функцию методу run():

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
$process->run(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Асинхронный запуск процессов

Вы можете также начать подпроцесс,а потом запустить его асихнронно, получая вывод и статус вашего основного процесса, когда они вам нужны. Используйте метод start(), чтобы начать асинхронный процсс, метод isRunning(), чтобы проверить, завершён ли процесс, и метод getOutput(), чтобы получить вывод:

1
2
3
4
5
6
7
8
$process = new Process('ls -lsa');
$process->start();

while ($process->isRunning()) {
    // ожидание окончания процесса
}

echo $process->getOutput();

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

1
2
3
4
5
6
7
8
$process = new Process('ls -lsa');
$process->start();

// ... делать другие вещи

$process->wait();

// ... делать вещи после завершения процесса

Note

Метод wait() - блокирующий, что означает, что ваш код будет остановлен на этой строке до тех пор, пока не будет завершён внешний процесс.

Note

Если Response отправлен до завершения дочернего процесса, то процесс сервера будет убит (в зависимости от вашей ОС). Это означает, что ваша задача будет моментально остановлена. Запуск асинхронного процсса не равняется запуску процесса, переживающего родительский процесс.

Если вы хотите, чтобы ваш процесс пережил цикл запрос / ответ, то вы можете воспользоваться преимуществами события kernel.terminate, и запустить вашу команду асинхронно внутри этого события. Имейте в виду, что kernel.terminate вызывает только, если выиспользуете PHP-FPM.

Caution

Также имейте в виду, что если вы это сделаете, то вышеназванный процесс PHP-МФП не будет доступен для обслуживания любого нового запроса до завершения подпроцесса. Это означает, что вы можете быстро заблокировать ваш МФП-пул, если вы не будете осторожны. Это то, почему обычно намного лучше не делать ничего мудрёного даже после отправки запроса, а вместо этого использвать очередь задач.

wait() берёт один необязательноый аргумент: обратный вызов, который постоянно вызывается во время работы процесса, передавая вывод и его тип:

1
2
3
4
5
6
7
8
9
10
$process = new Process('ls -lsa');
$process->start();

$process->wait(function ($type, $buffer) {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Потоковая передача в стандартный ввод процесса

До начала процесса вы можете указать его стандартный ввод, используя либо метод setInput(), либо 4й аргумент контруктора. Предоставленный ввод может быть строкой, источником потока или траверсабельным объектом:

1
2
3
$process = new Process('cat');
$process->setInput('foobar');
$process->run();

Когда этот ввод будет полностью написан в стандартном вводе подпроцесса, соответствущая труба будет закрыта.

Чтобы написать в стандартный ввод подроцесса во время его работы, компонент предоставляет класс InputStream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$input = new InputStream();
$input->write('foo');

$process = new Process('cat');
$process->setInput($input);
$process->start();

// ... прочитать вывод процесса или сделать что-то ещё

$input->write('bar');
$input->close();

$process->wait();

// отразит: foobar
echo $process->getOutput();

Метод write() принимает скалярные значения, источники потока или траверсабельные объекты в качестве аргумента. Как показано в примере выше, вам нужно ясно вызвать метод close(), когда вы закончите писать в стандартный ввод подпроцесса.

Использование PHP потоков в качестве стандартного ввода процесса

Ввод процесса может быть также определён с использованием PHP потоков:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$stream = fopen('php://temporary', 'w+');

$process = new Process('cat');
$process->setInput($stream);
$process->start();

fwrite($stream, 'foo');

// ... прочитать вывод процесса или сделать что-то другое

fwrite($stream, 'bar');
fclose($stream);

$process->wait();

// отразит: 'foobar'
echo $process->getOutput();

Остановка процесса

Любой асинхронный процесс можно остановить в любое время методом stop(). Этот метод берёт два аргумента: превышение лимита времени и сигнал. Когда лимит времени достигнут, сигнал отправляется текущему процессе. Сигнал по умолчанию, который отправляется процессу - SIGKILL. Пожалуйста, прочтите документацию сигнала ниже, чтобы узнать больше об обработке сигнала в компоненте Процесс:

1
2
3
4
5
6
$process = new Process('ls -lsa');
$process->start();

// ... сделать что-то другое

$process->stop(3, SIGINT);

Выполнение PHP кода в изоляции

Если вы хотите выполнить некоторый PHP код в изооляции, используйте вместо этого PhpProcess:

1
2
3
4
5
6
7
use Symfony\Component\Process\PhpProcess;

$process = new PhpProcess(<<<EOF
    <?php echo 'Hello World'; ?>
EOF
);
$process->run();

Превышение лимита времени процесса

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

1
2
3
4
5
use Symfony\Component\Process\Process;

$process = new Process('ls -lsa');
$process->setTimeout(3600);
$process->run();

Если лимит времени достигнут, то вызывается RuntimeException.

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

1
2
3
4
5
6
7
8
9
10
11
$process->setTimeout(3600);
$process->start();

while ($condition) {
    // ...

    // проверить, достигнут ли лимит времени
    $process->checkTimeout();

    usleep(200000);
}

Превышение лимита времени бездействия процесса

В отличие от превышения лимита времени в предыдущем параграфе, лимит бездействия процесса рассматривает только то время, которое прошло с момента последнего вывода, произведённого процессом:

1
2
3
4
5
6
use Symfony\Component\Process\Process;

$process = new Process('something-with-variable-runtime');
$process->setTimeout(3600);
$process->setIdleTimeout(60);
$process->run();

Вышеописанном случае, процесс считается законченным, когда либо общее количество времени работы превышает 3600 секунд, либо процесс не производит никакого вывода в течение 60 секунд.

Сигналы процесса

При асинхронным запуске программы, вы можете отправлять сигналы с помощью метода signal():

1
2
3
4
5
6
7
use Symfony\Component\Process\Process;

$process = new Process('find / -name "rabbit"');
$process->start();

// отправит SIGKILL процессу
$process->signal(SIGKILL);

Caution

В связи с некоторыми ограничениями в PHP, если вы используете сигналы с компонентом Процесс, то вам может понадобиться добавлять к вашим командам префикс exec. Пожалуйста, прочтите Symfony Issue#5759 и PHP Bug#39992, чтобы понять, почему так происходит.

POSIX сигналы недоступны на платформах Windows, пожалуйста, обратитесь к документации PHP для списка доступных сигналов.

Pid процесса

Вы можете получить доступ к pid текущего процесса с помощью метода getPid():

1
2
3
4
5
6
use Symfony\Component\Process\Process;

$process = new Process('/usr/bin/php worker.php');
$process->start();

$pid = $process->getPid();

Caution

В связи с некоторыми ограничениями в PHP, если вы используете сигналы с компонентом Процесс, то вам может понадобиться добавлять к вашим командам префикс exec. Пожалуйста, прочтите Symfony Issue#5759, чтобы понять, почему так происходит.

Отключение вывода

Так как стандартный вывод и вывод ошибок всегда извлекаются из основоположного процесса, может быть удобным отключить вывод в некоторых случаях для сохранения памяти. Используйте disableOutput() и enableOutput(), чтобы переключить эту функцию:

1
2
3
4
5
use Symfony\Component\Process\Process;

$process = new Process('/usr/bin/php worker.php');
$process->disableOutput();
$process->run();

Caution

Вы не можете включать или отключать вывод во время выполнения процесса.

Если вы отключите вывод, вы не сможете получить доступ к getOutput(), getIncrementalOutput(), getErrorOutput(), getIncrementalErrorOutput() или setIdleTimeout().

Однако, возможно передать обратный вызов методам start, run или mustRun, чтобы обработать процесс вывода в потоке.

Поиск выполняемого бинарного PHP файла

Этот компонент также предоставляет класс утилиты под названием PhpExecutableFinder, который возвращает абсолютный путь выполняемого бинарного PHP файла, доступного на вашем сервере:

1
2
3
4
5
use Symfony\Component\Process\PhpExecutableFinder;

$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();
// $phpBinaryPath = '/usr/local/bin/php' (результат будет другим на вашем компьютере)

Проверка поддержки TTY

Ещё одна функция предоставляемая этим компонентом - это метод isTtySupported(), который возвращает поддерживает ли текущая операционная система TTY:

1
2
3
use Symfony\Component\Process\Process;

$process = (new Process())->setTty(Process::isTtySupported());

4.1

The isTtySupported() method was introduced in Symfony 4.1.