Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

php

curl

sigint

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?

like image 322
Jonathan Avatar asked Apr 10 '18 17:04

Jonathan


2 Answers

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.

like image 79
Smuuf Avatar answered Oct 06 '22 08:10

Smuuf


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!

like image 43
Derek Pollard Avatar answered Oct 06 '22 08:10

Derek Pollard