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 + C, пока выполняется запрос CURL, он не действует. Команда просто зависает. Если я удалю свой пользовательский обработчик выключения, Ctrl + C немедленно уничтожит запрос CURL.

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

Ответы

Ответ 1

Что работает?

То, что действительно работает, дает все необходимое для работы с манерой обработки сигналов. Такое пространство, как представляется, обеспечивается путем включения обработки прогресса 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() в 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

Ответ 2

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

Когда вы отправляете команду 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: enter image description here

На изображении выше вы можете видеть, что когда я выдаю команду 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);
}

И затем запуск: enter image description here

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

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

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

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

Ответ 3

Если можно перейти на 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.