CURL cannot be killed by a PHP SIGINT with custom signal handler
Asked Answered
T

4

17

I have a PHP command line app with a custom shutdown handler:

<?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);
}

If I kill the script with Ctrl+C while a CURL request is in progress, it has no effect. The command just hangs. If I remove my custom shutdown handler, Ctrl+C kills the CURL request immediately.

Why is CURL unkillable when I define a SIGINT handler?

Troposphere answered 10/4, 2018 at 17:12 Comment(13)
Why do you think CURL is responding to the signal, rather than it just being completely ignored?Pluperfect
@Pluperfect Sorry, my question was maybe a bit misleading (I've updated). The crux of it is why is CURL unkillable in this scenario.Troposphere
Curl is an external library so killing it in the middle of an operation isn't going to be easy/possible from userland. You're going to have to figure out how to hook back into the default signal handler to get it killed properly.Elisabetta
sounds like a bug in the curl resource's cleanup routine, which happens implicitly at script exit - what happens if you change it to $shutdownHandler = function () use(&$ch){ echo 'Exiting'; curl_close($ch); echo "curl closed.\n"; exit(); }; ?Counterforce
Just so it's been asked: do you see the same issue with the ticks declaration removed?Proudhon
I think your underlying assumption might be wrong. I replaced the curl requests with a sleep(30) like : while ($testing) { echo "Sleeeeeping..." . PHP_EOL; sleep(10); echo "Awake!" . PHP_EOL; } doing a ctrl+C causes the sleep to halt and "Awake!" to get printed earlier than expected, but it returns right back to the loop. Even if I have $testing = false; in my signal handler.Proudhon
nvm, apparently those ticks do matter. shows what I know.Proudhon
This might be relevant : github.com/laravel/framework/issues/22301Proudhon
sounds like a bug in PHP (something like unable to call the SIGINT handler during curl_exec()), someone should take it to bugs.php.netCounterforce
Are you running Ubuntu >=16.04 ?Largo
@Largo this is on Ubuntu 16.04.3 LTS, running PHP 7.0.27.Troposphere
I think the answer lies in #26934716, the issue is your curl call is stuck with a blocking io and php can't reach to your signal handler at all and setting handler disables the original handler all togetherGonion
@Jonathan, it is not possible to do this without restructing the code in 7.0.X. See if this gist is a acceptable solution to you? gist.github.com/tarunlalwani/6b4f2b81f20c781234899e62f22b0436, if it is then only I will post an answerGonion
M
12

What does work?

What really seems to work is giving the whole thing some space to work its signal handling magic. Such space seems to be provided by enabling cURL's progress handling, while also setting a "userland" progress callback:

Solution 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);
}

Seems like there needs to be "something" in the progress callback function. Empty body does not seem to work, as it probably just doesn't give PHP much time for signal handling (hardcore speculation).


Solution 2

Putting pcntl_signal_dispatch() in the callback seems to work even without declare(ticks=1); on PHP 7.1.

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

Solution 3 ❤ (PHP 7.1+)

Using pcntl_async_signals(true) instead of declare(ticks=1); works even with empty progress callback function body.

This is probably what I, personally, would use, so I'll put the complete code here:

<?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);
}

All these three solutions cause the PHP 7.1 to quit almost instantly after hitting CTRL+C.

Mata answered 19/4, 2018 at 20:45 Comment(4)
It's not sleep(100). It's usleep(100). 100 microseconds = .1 miliseconds.Mata
Ok, I appologize for the misunderstanding, but I would suggest to use third solution if you don't see any disadvantages in that solution.Largo
The third solution will only work from 7.1.X or higher. Seems like v7.0.28-0ubuntu0.16.04.1 doesn't have itGonion
That's because pcntl_async_signals() is available only in PHP 7.1+.Mata
B
8

What is happening?

When you send the Ctrl + C command, PHP tries to finish the current action before exiting.

Why does my (OP's) code not exit?

SEE FINAL THOUGHTS AT THE END FOR A MORE DETAILED EXPLANATION

Your code does not exit because cURL doesn't finish, so PHP cannot exit until it finishes the current action.

The website you've chosen for this exercise never loads.

How to fix

To fix, replace the URL with something that does load, like https://google.com, for instance

Proof

I wrote my own code sample to show me exactly when/where PHP decides to exit:

<?php
declare(ticks=1);

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

pcntl_signal(SIGINT, $shutdownHandler);

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

When running this in the terminal, you get a much clearer idea of how PHP is operating: enter image description here

In the image above, you can see that when I issue the SIGINT command via Ctrl + C (shown by the arrow), it finishes up the action it is doing, then exits.

This means that if my fix is correct, all it should take to kill curl in OP's code, is a simple URL change:

<?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);
}

And then running: enter image description here

Viola! As expected, the script terminated after the current process was finished, like its supposed to.

Final thoughts

The site you're attempting to curl is effectively sending your code on a trip that has no end. The only actions capable of stopping the process are CTRL + X, the max execution time setting, or the CURLOPT_TIMEOUT cURL option. The reason CTRL+C works when you take OUT pcntl_signal(SIGINT, $shutdownHandler); is because PHP no longer has the burden of graceful shutdown by an internal function. Since PHP isn't concurrent, when you do have the handler in, it has to wait its turn before it is executed - which it will never get because the cURL task will never finish, thus leaving you with the never-ending results.

I hope this helps!

Barometer answered 19/4, 2018 at 22:14 Comment(4)
Thanks for your detailed response, however the choice of a slow loading URL is key to the question. If a signal handler is not defined a request will always be killed, even if it's in progress. This behaviour changes when the signal handler is defined and is what I'm interested in.Troposphere
@Troposphere I went into detail about why it doesn't work with your URL. please read the whole answer.Barometer
Your code does not exit because cURL doesn't finish, so PHP cannot exit until it finishes the current action. is literally the answer, the only fix is to change the URL.Barometer
The reason CTRL+C works when you take OUT pcntl_signal(SIGINT, $shutdownHandler); is because PHP no longer has the burden of graceful shutdown by an internal function. Since PHP isn't concurrent, when you do have the handler in, it has to wait its term before it is executed - which it will never get because the cURL task will never finish, thus leaving you with the never-ending results.Barometer
G
4

If it is possible to upgrade to PHP 7.1.X or higher, I would use the Solution 3 that @Smuuf posted. That is clean.

But if you can't upgrade then you need to use a workaround. The issue happens as explained in the below SO thread

pcntl_signal function not being hit and CTRL+C doesn't work when using sockets

PHP is busy in a blocking IO and it cannot process the pending signal for which you have customized the handler. Anytime a signal is raised and you have handler associated with it, PHP queues the same. Once PHP has completed its current operation then it executes your handler.

But unfortunately in this situation it never gets that chance, you don't specify a timeout for your CURL and it is just a blackhole which PHP is not able to escape.

So if you can have one php process have the responsibility of handling the SIGTERM and one child process having to handle the work then it would work, as the parent process will be able to catch the signal and process the callback even when child is occupied with the same

Below is a code which demonstrates the same

<?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);
        }
    }

And below is it in action

PHP Kill

Gonion answered 21/4, 2018 at 5:31 Comment(0)
L
-2

I've scratched my head over this for a while and didn't find any optimal solutions using pcntl but depending on the intended usage of your command you may consider :

  • Using a timeout :
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
    As you may already know there are many options to control the behavior of curl, including setting a progress callback function.
  • Using file_get_contents() instead of curl.

A solution using the Ev extension :

For the signal handling code, it could be as simple as this :

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

A quick summary of the installation of the Ev extension :
sudo pecl install Ev
You will need to enable the extension by adding to the php.ini file of the php cli, may be /etc/php/7.0/cli/php.ini
extension="ev.so"
If pecl complain about missing phpize, install php7.0-dev.

Largo answered 16/4, 2018 at 17:38 Comment(1)
Downvote it or not this a better answer than you can.Largo

© 2022 - 2024 — McMap. All rights reserved.