Компонент Process

Дата обновления перевода: 2024-07-10

Компонент Process

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

Установка

1
$ composer require 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\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

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

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

echo $process->getOutput();

Метод 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

Компонент Process внутренне использует 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();
}

Tip

Вы можете получить время последнего вывода в секундах с помощью метода getLastOutputTime(). Этот метод возвращает null, если процесс не был запущен!

Конфигурация опций процесса

Для запуска процессов Symfony использует функцию proc_open. Вы можете сконфигурировать параметры, передаваемые аргументу other_options proc_open(), используя метод setOptions():

1
2
3
$process = new Process(['...', '...', '...']);
// эта опция позволяет подпроцессу продолжаться после того, как основной скрипт завершил работу
$process->setOptions(['create_new_console' => true]);

Caution

Большинство опций, определенных proc_open() (например,
create_new_console и подавление_ошибок), поддерживаются только в операционных системах Windows. Перед их использованием ознакомьтесь с документацией PHP для proc_open().

Использование функций из оболочки ОС

Использование массива аргументов является рекомендуемым способом определения команд. Это избавляет вас от экранирования и позволяет беспрепятственно посылать сигналы (например, для остановки запущенных процессов):

1
2
$process = new Process(['/path/command', '--option', 'argument', 'etc.']);
$process = new Process(['/path/to/php', '--define', 'memory_limit=1024M', '/path/to/script.php']);

Если вам необходимо потоково передавать перенаправления, условное выполнение или любую другую функцию, предоставленную оболочкой вашей операционной системы, вы также можете определить команды в виде строк с помощью статичной фабрики fromShellCommandline().

Каждая операционная система предоставляет свой синтаксис для командных строк, поэтому за экранирование и переносимость придётся отвечать вам.

При использовании строк для определения команд аргументы переменных передаются как переменные окружения, используя второй аргумент методов run(), mustRun() или start(). Ссылание на них также зависит от ОС:

1
2
3
4
5
6
7
8
// На ОС, схожих с Unix (Linux, macOS)
$process = Process::fromShellCommandline('echo "$MESSAGE"');

// На Windows
$process = Process::fromShellCommandline('echo "!MESSAGE!"');

// На ОС, схожих с Unix-like и Windows
$process->run(null, ['MESSAGE' => 'Something to output']);

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

1
2
// работает одинаково на Windows , Linux и macOS
$process = Process::fromShellCommandline('echo "${:MESSAGE}"');

Переносимые команды требуют использования синтаксиса, характерного для данного компонента: при заключении имени переменной точно в ${: и }, объект процесса заменит её экранированным значением, либо потерпит неудачу, если переменная не будет найдена в списке переменных окружения, прилагаемых к команде.

Установка переменных окружения для процессов

Конструктор класса Process и все его методы, связанные с выполнением процессов (run(), mustRun(), start() и т.д.) позволяют передавать массив переменных окружения, которые необходимо установить при выполнении процесса:

1
2
3
$process = new Process(['...'], null, ['ENV_VAR_NAME' => 'value']);
$process = Process::fromShellCommandline('...', null, ['ENV_VAR_NAME' => 'value']);
$process->run(null, ['ENV_VAR_NAME' => 'value']);

Помимо явно переданных переменных окружения, процессы наследуют все переменные окружения, определённые в вашей системе. Вы можете предотвратить это, установив значение false для тех переменных окружения, которые вы хотите удалить:

1
2
3
4
$process = new Process(['...'], null, [
    'APP_ENV' => false,
    'SYMFONY_DOTENV_VARS' => false,
]);

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

При выполнении длительной команды (например, rsync на удалённом сервере сервер), вы можете дать обратную связь конечному пользователю в реальном времени, передав анонимную функцию методу 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): void {
    if (Process::ERR === $type) {
        echo 'ERR > '.$buffer;
    } else {
        echo 'OUT > '.$buffer;
    }
});

Note

Эта функция не будет работать ожидаемо на серверах, использующих буферизацию вывода PHP. В этих случаях следует либо отключить PHP-опцию output_buffering, либо использовать PHP-функцию ob_flush для принудительной отправки буфера вывода.

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

Вы можете также начать подпроцесс,а потом запустить его асихнронно, получая вывод и статус вашего основного процесса, когда они вам нужны. Используйте метод 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.

Danger

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

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

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

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

Вместо того чтобы ждать завершения процесса, вы можете использовать метод waitUntil() для продолжения или прекращения ожидания, основываясь на некоторой логике PHP. В следующем примере запускается долго выполняющийся процесс и проверяется его вывод, чтобы дождаться его полной инициализации:

1
2
3
4
5
6
7
8
9
10
11
$process = new Process(['/usr/bin/php', 'slow-starting-server.php']);
$process->start();

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

// ожидает, пока заданная анонимная функция не вернёт true
$process->waitUntil(function ($type, $output): bool {
    return $output === 'Ready. Waiting for commands...';
});

// ... сделать что-то после того, как процесс будет готов

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

До начала процесса вы можете указать его стандартный ввод, используя либо метод 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();

Использование режимов TTY и PTY

Все приведенные примеры показывают, что ваша программа имеет контроль над вводом процесса (используя setInput()) и выводом из этого процесса (используя getOutput()). Компонент Process имеет два специальных режима, которые настраивают отношения между вашей программой и процессом: телетайп (tty) и псевдотелетайп (pty).

В режиме TTY вы соединяете ввод и вывод процесса с вводом и выводом вашей программы. Это позволяет, например, открыть редактор типа Vim или Nano в качестве процесса. Вы включаете режим TTY, вызывая setTty():

1
2
3
4
5
6
7
$process = new Process(['vim']);
$process->setTty(true);
$process->run();

// Так как вывод соединён с терминалом, больше невозможно прочитать
// или изменить вывод из этого процесса!
dump($process->getOutput()); // null

В режиме PTY ваша программа ведёт себя как терминал для процесса, а не как обычный ввод и вывод. Некоторые программы ведут себя по-разному при взаимодействии с реальным терминалом, а не с другой программой. Например, некоторые программы при общении с терминалом запрашивают пароль. Используйте setPty() для включения этого режима.

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

Любой асинхронный процесс можно остановить в любое время методом 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
    <?= 'Hello World' ?>
EOF
);
$process->run();

Выполнение дочернего PHP-процесса с той же конфигурацией

Когда вы запускаете процесс PHP, он использует конфигурацию по умолчанию, определённую в вашем файле php.ini. Вы можете обойти эти опции с помощью опции командной строки -d. Например, если memory_limit установлен как 256M, то можно отключить это ограничение памяти при выполнении какой-либо команды, например, такой: php -d memory_limit=-1 bin/console app:my-command.

Однако если запустить команду через класс Symfony Process, то PHP будет использовать настройки, определённые в файле php.ini. Эту проблему можно решить, используя класс PhpSubprocess для запуска команды:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Process\Process;

class MyCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        // memory_limit (и любые другие опции конфигурации) этой команды - те, которые
        // определены в php.ini вместо новых значений (опционально), переданных через
        // опцию команды '-d'
        $childProcess = new Process(['bin/console', 'cache:pool:prune']);

        // memory_limit (и любые другие опции конфигурации) этой команды берут во
        // внимание значения (опционально), переданные через опцию команды '-d'
        $childProcess = new PhpSubprocess(['bin/console', 'cache:pool:prune']);
    }
}

Тайм-аут процесса

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

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

Tip

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

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

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

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);

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();

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

Так как стандартный вывод и вывод ошибок всегда извлекаются из основоположного процесса, может быть удобным отключить вывод в некоторых случаях для сохранения памяти. Используйте 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\ExecutableFinder;

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

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

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

$executableFinder = new ExecutableFinder();
$chromedriverPath = $executableFinder->find('chromedriver', '/path/to/chromedriver', ['local-bin/']);

Поиск выполняемой 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());