Работа с внешними процессами через proc_open в PHP

Раздел: Программирование на PHP -> Функции 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]
- написать функцию php (создание функции в php)
- Php глобальные функции (глобальные функции в php)
- Php proc open (функция proc_open в php)

Расширенные примеры использования 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

Функция proc_open в PHP - comments

En
Php proc open (php)