CURL не может быть уничтожен PHP SIGINT с пользовательским обработчиком сигнала

У меня есть приложение командной строки PHP с пользовательским обработчиком выключения:

<?php
declare(ticks=1);

$shutdownHandler = function () {
echo 'Exiting';
exit();
};

pcntl_signal(SIGINT, $shutdownHandler);

while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}

Если я убью сценарий с Ctrl+С пока выполняется запрос CURL, он не имеет никакого эффекта. Команда просто висит. Если я удалю свой пользовательский обработчик выключения, Ctrl+С убивает запрос CURL немедленно.

Почему CURL неубиваем, когда я определяю SIGINT обработчик?

14

Решение

То, что действительно похоже на работу, дает всему пространству некоторое пространство для магии обработки сигналов. Такое пространство, по-видимому, обеспечивается за счет включения обработки прогресса cURL, а также установки обратного вызова прогресса «пользовательского пространства»:

Решение 1

while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_NOPROGRESS, false); // "true" by default
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {
usleep(100);
});
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}

Похоже, должно быть что-то в функции обратного вызова прогресса. Пустое тело, похоже, не работает, так как, вероятно, просто не дает PHP много времени для обработки сигналов. (жесткое предположение).


Решение 2

Ввод pcntl_signal_dispatch() в обратном вызове, кажется, работает даже без declare(ticks=1); на PHP 7.1,

...
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {
pcntl_signal_dispatch();
});
...

Решение 3 ❤ (PHP 7.1+)

С помощью pcntl_async_signals(true) вместо declare(ticks=1); работает даже с пустым телом функции обратного вызова прогресса.

Это, вероятно, то, что я лично использовал бы, поэтому я поставлю полный код здесь:

<?php

pcntl_async_signals(true);

$shutdownHandler = function() {
die("Exiting\n");
};

pcntl_signal(SIGINT, $shutdownHandler);

while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function() {});
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}

Все эти три решения вызывают PHP 7.1 в почти сразу после удара CTRL+C.

8

Другие решения

Что происходит?

Когда вы отправляете Ctrl + C команда, PHP пытается завершить текущее действие перед выходом.

Почему мой код (OP) не завершается?

ПОСМОТРЕТЬ ЗАКЛЮЧИТЕЛЬНЫЕ МЫСЛИ В КОНЦЕ ДЛЯ БОЛЬШЕ ПОДРОБНОГО ОБЪЯСНЕНИЯ

Ваш код не выходит, потому что cURL не заканчивается, так PHP не может выйти, пока не завершит текущее действие.

Веб-сайт, который вы выбрали для этого упражнения, никогда не загружается.

Как исправить

Чтобы исправить, замените URL на что-то, что загружается, например https://google.com, например

доказательство

Я написал свой собственный пример кода, чтобы показать мне, когда и где PHP решает выйти:

<?php
declare(ticks=1);

$t = 1;
$shutdownHandler = function () {
exit("EXITING NOW\n");
};

pcntl_signal(SIGINT, $shutdownHandler);

while (true) {
print "$t\n";
$t++;
}

Запустив это в терминале, вы получите более четкое представление о том, как работает PHP:
введите описание изображения здесь

На изображении выше вы можете видеть, что когда я выполняю команду SIGINT через Ctrl + C (показано стрелкой), завершает действие, которое выполняет, затем завершает работу.

Это означает, что если мое исправление верное, все, что нужно, чтобы убить curl в коде OP, это просто URL менять:

<?php

declare(ticks=1);
$t = 1;
$shutdownHandler = function () {
exit("\nEXITING\n");
};

pcntl_signal(SIGINT, $shutdownHandler);


while (true) {
echo "CURL$t\n";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://google.com');
curl_exec($ch);
curl_close($ch);
}

А потом работает:
введите описание изображения здесь

Виола! Как и ожидалось, скрипт завершился после завершения текущего процесса, как и предполагалось.

Последние мысли

Сайт, который вы пытаетесь свернуть, эффективно отправляет ваш код в путешествие, которое не имеет конца. Единственные действия, способные остановить процесс CTRL + X, максимальное время выполнения настройка или CURLOPT_TIMEOUT вариант cURL. Причина CTRL+C работает, когда вы берете pcntl_signal(SIGINT, $shutdownHandler); потому что PHP больше не несет бремени постепенного отключения внутренней функцией. Поскольку PHP не работает одновременно, когда у вас есть обработчик, он должен ждать своей очереди, прежде чем он будет выполнен — ​​что он никогда не получит, потому что cURL задание никогда не закончится, оставляя вас с бесконечными результатами.

Надеюсь, это поможет!

7

Если возможно обновить до PHP 7.1.X или выше, я бы использовал Solution 3 что @Smuuf опубликовал. Это чисто.

Но если вы не можете обновить, то вам нужно использовать обходной путь. Проблема происходит, как объяснено в приведенной ниже ветке SO

Функция pcntl_signal не была нажата, а CTRL + C не работает при использовании сокетов

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

Но, к сожалению, в этой ситуации у него никогда не будет такого шанса, вы не указываете таймаут для своего CURL, и это просто черная дыра, от которой PHP не может уйти.

Так что если вы можете иметь один процесс PHP, несут ответственность за обработку SIGTERM и один дочерний процесс должен обрабатывать работу, тогда он будет работать, так как родительский процесс сможет перехватить сигнал и обработать обратный вызов, даже если дочерний процесс занят тем же

Ниже приведен код, который демонстрирует то же самое

<?php
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// we are the parent
declare (ticks = 1);
//pcntl_async_signals(true);
$shutdownHandler = function()
{
echo 'Exiting' . PHP_EOL;
};
pcntl_signal(SIGINT, $shutdownHandler);
pcntl_wait($status); //Protect against Zombie children
} else {
// we are the child
echo "hanging now" . PHP_EOL;
while (true) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://blackhole.webpagetest.org');
curl_exec($ch);
curl_close($ch);
}
}

И ниже это в действии

PHP Kill

4

Некоторое время я ломал голову над этим и не нашел оптимальных решений с использованием pcntl, но в зависимости от предполагаемого использования вашей команды вы можете рассмотреть следующие вопросы:

Решение с использованием расширения Ev:

Для кода обработки сигналов это может быть так просто:

$w = new EvSignal(SIGINT, function (){
exit("SIGINT received\n");
});
Ev::run();

Краткий обзор установки расширения Ev:
sudo pecl install Ev
Вам нужно будет включить расширение, добавив в php.ini файл php cli, может быть /etc/php/7.0/cli/php.ini
extension="ev.so"
Если pecl жалуется на отсутствие phpize, установите php7.0-dev.

-2
По вопросам рекламы [email protected]