Работа с внешними процессами через proc_open в PHP
Основы работы с proc_open
Функция proc_open позволяет запускать внешние процессы и управлять их потоками ввода-вывода. Это мощный инструмент для взаимодействия с системными командами, интерпретаторами и другими программами.
Базовое открытие процесса и чтение вывода
Самый простой способ - запустить команду и прочитать её стандартный вывод. Для этого используются дескрипторы, описывающие потоки.
$descriptorspec = [
0 => ['pipe', 'r'], // stdin - мы будем писать
1 => ['pipe', 'w'], // stdout - мы будем читать
2 => ['pipe', 'w'] // stderr - тоже читаем
];
$process = proc_open('ls -la', $descriptorspec, $pipes);
if (is_resource($process)) {
// читаем стандартный вывод
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// закрываем процесс
$return_code = proc_close($process);
echo "Вывод:\n$stdout";
echo "Ошибки (если есть):\n$stderr";
echo "Код возврата: $return_code";
}
функции работы с массивом php (функции для работы с массивами в php)
Вывод: итого 48 drwxr-xr-x 2 user user 4096 янв 18 12:00 . ... Код возврата: 0
функция file php (функция file() в php)
Пояснение: Массив $descriptorspec описывает три потока: 0 (stdin), 1 (stdout), 2 (stderr). Каждый поток может быть pipe (канал) или file. После запуска proc_open возвращает ресурс процесса и заполняет массив $pipes тремя ресурсами. Важно закрыть все потоки до вызова proc_close, иначе может возникнуть блокировка.
Как выполнить команду и получить только stdout?
Если stderr не нужен, можно перенаправить его в /dev/null или в файл.
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['file', '/dev/null', 'a']
];
$process = proc_open('php -v', $descriptorspec, $pipes);
if (is_resource($process)) {
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
echo "Версия PHP: $stdout";
}
функция get php (функция get() в php)
Как передать данные на вход процессу (stdin)?
Иногда нужно отправить текст команде, например, в консольный редактор или калькулятор.
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open('bc', $descriptorspec, $pipes);
if (is_resource($process)) {
fwrite($pipes[0], "2^10\n");
fclose($pipes[0]); // обязательно закрыть вход
$result = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
echo "Результат: $result";
}
функция php выводит данные на экран (вывод данных на экран в php)
Результат: 1024
статическая функция php (статические методы в php)
Почему важно закрыть stdin сразу после записи?
Если не закрыть, процесс может ждать дальнейшего ввода и не завершиться, что приведёт к блокировке.
Как одновременно читать stdout и stderr без блокировки?
При больших объёмах данных может возникнуть взаимная блокировка (deadlock). Для этого используют неблокирующее чтение.
$process = proc_open('grep error /var/log/syslog', $descriptorspec, $pipes);
if (is_resource($process)) {
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
$stdout = '';
$stderr = '';
$timeout = 5;
$start = time();
while (true) {
$r = [$pipes[1], $pipes[2]];
$w = null;
$e = null;
if (stream_select($r, $w, $e, 0, 200000) === false) {
break;
}
foreach ($r as $stream) {
if ($stream === $pipes[1]) {
$stdout .= fread($pipes[1], 4096);
} else {
$stderr .= fread($pipes[2], 4096);
}
}
if (feof($pipes[1]) && feof($pipes[2])) break;
if (time() - $start > $timeout) {
proc_terminate($process);
break;
}
}
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
}
Php функции даты (функции даты в php)
Как получить статус процесса до его полного завершения?
Функция proc_get_status возвращает информацию о процессе, включая PID и код выхода (если завершился). Вызывать её нужно несколько раз, пока процесс не завершится.
$process = proc_open('sleep 2', $descriptorspec, $pipes);
$status = proc_get_status($process);
echo "PID: " . $status['running'] ? 'работает' : 'завершён';
// подождать
sleep(3);
$status = proc_get_status($process);
echo "После ожидания: " . ($status['running'] ? 'работает' : 'завершён');
echo "Код выхода: " . $status['exitcode'];
proc_close($process);
функция return php (оператор return в функциях php)
Типичные ошибки и их решение
- Блокировка при чтении/записи: если не закрыть поток, который записывает, процесс может зависнуть. Решение - использовать неблокирующий режим или закрывать потоки своевременно.
- Неверный дескриптор: попытка читать из закрытого или несуществующего потока. Проверяйте ресурсы через
is_resource. - Утечка дескрипторов: если процесс не завершён, а PHP-скрипт заканчивается, ресурсы могут остаться в системе. Всегда вызывайте
proc_closeилиproc_terminate. - Проблемы с правами: команда может потребовать права root или быть недоступной. Проверяйте с помощью
is_executableили try-catch. - Ошибка буферизации: при работе с интерактивными программами (например, ssh) может потребоваться псевдотерминал (
pty). Для этого используйте['pty']в дескрипторах.
Как запустить процесс с перенаправлением потока ошибок в stdout?
Для этого в дескрипторах можно указать, что поток 2 совпадает с потоком 1.
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
// Но можно объединить stdout и stderr в команде:
$process = proc_open('command 2>&1', $descriptorspec, $pipes);
// Тогда читаем только из pipes[1]
Расширенные примеры использования proc_open
Запуск нескольких процессов параллельно и сбор результатов
Иногда требуется запустить несколько команд одновременно, например, для параллельной обработки файлов. Используем массив процессов.
$commands = [
'echo "Процесс 1"',
'echo "Процесс 2"',
'sleep 1; echo "Процесс 3"'
];
$processes = [];
$pipes = [];
foreach ($commands as $index => $cmd) {
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open($cmd, $descriptorspec, $pipes[$index]);
$processes[$index] = $process;
// сразу закрываем stdin, так как писать не собираемся
fclose($pipes[$index][0]);
}
// Читаем результаты после запуска всех
$outputs = [];
foreach ($processes as $index => $process) {
$stdout = stream_get_contents($pipes[$index][1]);
fclose($pipes[$index][1]);
$stderr = stream_get_contents($pipes[$index][2]);
fclose($pipes[$index][2]);
$exitcode = proc_close($process);
$outputs[$index] = [
'stdout' => $stdout,
'stderr' => $stderr,
'exitcode' => $exitcode
];
}
print_r($outputs);
Array
(
[0] => Array
(
[stdout] => Процесс 1
[stderr] =>
[exitcode] => 0
)
[1] => Array
(
[stdout] => Процесс 2
[stderr] =>
[exitcode] => 0
)
[2] => Array
(
[stdout] => Процесс 3
[stderr] =>
[exitcode] => 0
)
)
Пояснение: Запускаем все процессы сразу, затем последовательно читаем вывод. Это позволяет экономить время на ожидании каждого процесса по очереди.
Взаимодействие с интерактивной программой (например, bash)
Можно организовать диалог с оболочкой: отправлять команды и получать ответы.
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open('/bin/bash', $descriptorspec, $pipes);
if (is_resource($process)) {
fwrite($pipes[0], "echo 'Привет, мир!'\n");
fwrite($pipes[0], "ls -d /home/*\n");
fwrite($pipes[0], "exit\n");
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($process);
echo "Вывод bash:\n$output";
if ($errors) echo "Ошибки:\n$errors";
}
Вывод bash: Привет, мир! /home/user /home/guest
Важно: при работе с интерактивными программами нужно учитывать, что они могут ждать ввода после каждой команды. В примере команды отправлены одной пачкой, затем закрыт stdin, и bash завершится после exit.
Использование псевдотерминала (pty) для программ, требующих tty
Некоторые программы (например, ssh, sudo) требуют наличия терминала. Используем дескриптор 'pty'.
$descriptorspec = [
0 => ['pty'],
1 => ['pty'],
2 => ['pty']
];
$process = proc_open('ssh user@localhost', $descriptorspec, $pipes);
if (is_resource($process)) {
// ждём приглашение
sleep(1);
fwrite($pipes[0], "password\n");
fwrite($pipes[0], "whoami\n");
fwrite($pipes[0], "exit\n");
fclose($pipes[0]);
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
echo $output;
}
Обратите внимание: при использовании pty все потоки (stdin, stdout, stderr) объединяются в один терминальный поток, поэтому читать надо только из pipes[1] (или pipes[0] в зависимости от реализации).
Обработка таймаута и принудительное завершение процесса
Если команда выполняется слишком долго, нужно остановить её принудительно.
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open('ping -c 100 google.com', $descriptorspec, $pipes);
$timeout = 5;
$start = time();
$output = '';
while (true) {
$r = [$pipes[1]];
$w = null;
$e = null;
$tv_sec = 0;
$tv_usec = 500000; // 0.5 сек
if (@stream_select($r, $w, $e, $tv_sec, $tv_usec) === false) {
break;
}
if ($r) {
$output .= fread($pipes[1], 8192);
}
if (feof($pipes[1])) break;
if (time() - $start > $timeout) {
proc_terminate($process, 9); // SIGKILL
echo "Процесс прерван по таймауту.\n";
break;
}
}
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
echo $output;
Использование proc_open с временными файлами для больших объёмов данных
Если нужно передать много данных, лучше использовать файлы вместо pipe, чтобы избежать переполнения буфера.
$tempFile = tempnam(sys_get_temp_dir(), 'proc');
file_put_contents($tempFile, "много данных...");
$descriptorspec = [
0 => ['file', $tempFile, 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open('wc -l', $descriptorspec, $pipes);
if (is_resource($process)) {
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
echo "Количество строк в переданных данных: $stdout";
}
unlink($tempFile);
Получение статуса процесса с помощью proc_get_status в цикле
Для отслеживания длительного процесса можно опрашивать его статус.
$process = proc_open('sleep 3', $descriptorspec, $pipes);
fclose($pipes[0]);
$running = true;
while ($running) {
$status = proc_get_status($process);
$running = $status['running'];
if ($running) {
echo "Процесс ещё работает...\n";
sleep(1);
}
}
echo "Завершён с кодом: " . $status['exitcode'];
proc_close($process);
Процесс ещё работает... Процесс ещё работает... Процесс ещё работает... Завершён с кодом: 0