1

I am doing a minor experiment with PHP. I have a file named 'infinity.txt', and in that file, I write an incrementing number every 0.25 seconds.

while(true){

    file_put_contents('infinity.txt', ++$i.PHP_EOL, FILE_APPEND);
    usleep(250 * 1000);

}

This works fine and well, but when I close the tab in my browser, the script continues running. A browser request abort is no Ctrl. + C, so there's no surprise in that, but I was still wondering whether it is possible to abort an infinite loop when the user disconnects, or if there is any method to see if the user is still connected.

Calling register_shutdown_function beforehand is completely useless, by the way, even when the linked function has a die() within.

UPDATE: It seems to me that something might be done using the connection_aborted() function.

UPDATE 2: I have changed my code to look like this, but alas, the infinite loop does not cancel:

while(true){

    if(connection_aborted()){

        file_put_contents('infinity.txt', 'CONNECTION ABORTED.', FILE_APPEND);
        die();

    }

    file_put_contents('infinity.txt', ++$i.PHP_EOL, FILE_APPEND);
    usleep(250 * 1000);

}

UPDATE 3: I am now echoing and flushing some text in every iteration, yet still to no avail:

while(true){

    echo '0';
    ob_flush();
    flush(); // necessary for proper checking

    if(connection_aborted()){

        file_put_contents('infinity.txt', 'CONNECTION ABORTED.', FILE_APPEND);
        die();

    }

    file_put_contents('infinity.txt', ++$i.PHP_EOL, FILE_APPEND);
    usleep(250 * 1000);

}

UPDATE 4: The code shown in the previous update started working for some weird reason upon restarting Apache, without me changing any settings in the php.ini. What ultimately helped was adding an ignore_user_abort(true); at the very beginning.

arik
  • 28,170
  • 36
  • 100
  • 156

2 Answers2

2

Do this way (I left explanation in comments):

ignore_user_abort(1); //we'll handle it by ourselves
header('Transfer-Encoding:chunked'); //HTTP 1.1: do not capture immediately (bin)
flush();
ob_flush();

$i = 0;
set_time_limit(0);
while(1)
{
    echo "0"; //do this: sending data to dead TCP connection will fail
    flush();
    ob_flush();
    if(connection_status()!=0)
    {

        file_put_contents('infinity.txt', 'CONNECTION ABORTED.', FILE_APPEND);
        echo "0\r\n\r\n"; //stream termination packet (double \r\n according to proto)
        flush();
        ob_flush();
        exit();

    }

    file_put_contents('infinity.txt', ++$i.PHP_EOL, FILE_APPEND);
    usleep(250 * 1000);

}
Alma Do
  • 37,009
  • 9
  • 76
  • 105
  • I've just restarted Apache, checked it in Firefox, and then back in Chrome. It started working with the code I was using in the third update. I'm confused, but thanks, ultimately, it was the `ob_flush(); flush();` that did the trick! Sorry for confusing you. – arik Mar 25 '14 at 14:32
  • Not only. Difference is that your third code hasn't `Transfer-Encoding` header (thus you'll receive your `0` because of `flush()` call). Also you need to send termination packet to end stream properly – Alma Do Mar 25 '14 at 14:34
  • 1
    No, the script works without the Transfer-Encoding header. That header is irrelevant. It was adding the `ignore_user_abort(true);` you suggested that did the trick. Thanks! – arik Mar 25 '14 at 14:39
  • It works. But will send you the data (i.e. browser will show it once it's received) – Alma Do Mar 25 '14 at 14:47
  • True, but it was never my intention in the first place to let the browser see the output the moment it is sent ;) – arik Mar 25 '14 at 14:51
1

See this comment on the same connection_aborted() page you linked.

The default procedure for a web server like PHP is to create the entire HTML page, and when finished, ship it to the browser. After the browser asks the server for the file, there is no further communication until the file is shipped.

You can, however, change this behavior with ob_flush() and related output control functions. This causes PHP to generate a certain amount of HTML, then ship it to the browser. This communication while processing allows your previously attempted connection_aborted() to work.

Edit to add: Glad you got it working! I also created some working test code for anybody else looking for this:

<?php 

@apache_setenv('no-gzip', 1);
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
for ($i = 0; $i < ob_get_level(); $i++) { ob_end_flush(); }
ob_implicit_flush(1);
ignore_user_abort(true);
ob_start();

for ($i=1;$i<10;$i++) {
    sleep(2);
    echo "done pass ". $i . ".<br />";
    echo str_pad('',4096)."\n";          
    ob_flush();
    if(connection_aborted()){
        file_put_contents('test.txt', "CONNECTION ABORTED on pass $i.".PHP_EOL, FILE_APPEND);
        exit(); 
    }
    else {
        file_put_contents('test.txt', "successful pass    on pass $i.".PHP_EOL, FILE_APPEND);
    }
}
Digital Chris
  • 6,177
  • 1
  • 20
  • 29
  • See my 3rd update. Unfortunately, it still doesn't work. – arik Mar 25 '14 at 14:08
  • The first step is to make sure progressive rendering is working: http://stackoverflow.com/questions/3133209/how-to-flush-output-after-each-echo-call Once you can see output in your browser, test the connection_aborted() part. I had to include padding to get it working like described here: http://us1.php.net/flush#54841 actually this may be good test code to start from. – Digital Chris Mar 25 '14 at 14:13
  • I've just restarted Apache, checked it in Firefox, and then back in Chrome. It started working with the code I was using in the third update. I'm confused, but thanks, ultimately, it was the `ob_flush(); flush();` that did the trick! – arik Mar 25 '14 at 14:30