Компонент Process

Дата оновлення перекладу 2024-05-035

Компонент 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 використовує PHP-функцію 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 та suppress_errors) підтримуються лише в операційних системах 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 та 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,
]);

Отримання виведення процесу у реальному часі

При виконанні довгострокової команди (на кшталт синхронизації файлів з віддаленим сервером), ви можете надати зворотній звʼязок з кінцевим користувачем у реальному часі, передавши анонімну функцію методу 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. Будь ласка, прочитайте документацію сигналу нижче , щоб дізнатися більше про обробку сигналу в компоненті Process:

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

Виконання дочірнього 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(['find', '/', '-name', 'rabbit']);
$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, щоб обробити процес виведення в потоці.

Пошук виконуваного

Цей компонент також надає клас утиліти під назвою ExecutableFinder, який знаходить абсолютний шлях виконуваного, доступного на вашому сервері:

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

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

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