3

I am trying to continue a PHP Script after the page/connection is closed.

Users will POLL the script in every 1 hour, I want to return some json output and want to continue the script in the background. I am using a shared host and I cannot use cron job.

Here is what I've tried.

ob_start();

ignore_user_abort();

echo "JSON_OUTPUT GOES HERE";

$ob_length = ob_get_length();

header("Content-Type : text/plain",TRUE);
header("Content-Length : $ob_length",TRUE);
header("Connection : Close",TRUE);

flush();
ob_flush();
ob_end_flush();

sleep(3);

echo "You cant see me..";

exit();

I am using Codeigniter framework, But its not working on my live server. It waits 3 seconds and then outputting You cant see me.. too.

Please help me.

Note

Project is hosted in LINUX/WINDOWS/WAMP-SERVER shared hosts.

Sven Tore
  • 967
  • 6
  • 29
Red
  • 6,230
  • 12
  • 65
  • 112
  • a first error that i can see i that header() function must be called before any actual output is sent, either by normal HTML tags, blank lines in a file, or from PHP [php manual](http://php.net/manual/en/function.header.php) – LuckyStarr Apr 30 '13 at 08:10
  • @lucky88 OP's already using [output buffering](http://www.php.net/manual/en/book.outcontrol.php). – Passerby Apr 30 '13 at 08:12
  • 2
    Why not run a PHP script (or even better, a shell script or something better suited for daemon-like tasks) in the background using cron, and have the poll script simply query its progress? Feels much more elegant. – Pekka Apr 30 '13 at 08:28
  • @Pekka웃 Reason:Shared Godaddy server. – Red Apr 30 '13 at 08:30
  • Isn't Godaddy limiting your scripts' maximum running time? What is the script doing in the background? – Pekka Apr 30 '13 at 08:32
  • @Pekka웃 the script is fetching emails from email servers. its very limited server. – Red Apr 30 '13 at 08:34
  • Then the `max_execution_time` for your scripts is likely to be limited as well. I'd check that out first if I were you – Pekka Apr 30 '13 at 09:17
  • @Pekka웃 its not my headache, i already mentioned this to my client :) – Red Apr 30 '13 at 09:24

4 Answers4

11

After some research i got it work, Sometime it may be useful to some others.

function closeOutput($stringToOutput){   
        set_time_limit(0);
        ignore_user_abort(true);
        header("Connection: close\r\n");
        header("Content-Encoding: none\r\n");  
        ob_start();          
        echo $stringToOutput;   
        $size = ob_get_length();   
        header("Content-Length: $size",TRUE);  
        ob_end_flush();
        ob_flush();
        flush();   
} 

You can use it like

$outputContent = 'Contentent Goes Here...';

closeOutput( $outputContent );

sleep(5);

//do some background works ...

exit();
Red
  • 6,230
  • 12
  • 65
  • 112
2

First, don't use space after Connection and before : it should be Header: value not Header : value. Second, Connection: close don't force browser to stop getting current response and display blank page. Here http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html chapter 14.10 it states: Connection: close in either the request or the response header fields indicates that the connection SHOULD NOT be considered 'persistent' (section 8.1) after the current request/response is complete

So how can you try if your code works:

ignore_user_abort();
header("Content-Type: text/plain; charset=UTF-8");

// just to try show following echo immediately, working depends on server configuration
while (@ob_end_flush()); 

echo date('Y-m-d H:i:s'), PHP_EOL;

echo "JSON_OUTPUT GOES HERE", PHP_EOL;

sleep(10); // 10 seconds so you can close browser tab before

// this file should be created after 10 seconds, even after you closed browser tab
// also check if permissions to write to __DIR__ are set for apache.
file_put_contents(__DIR__ . '/tmp.txt', "Text after 10 sec");

exit;

Open this php file in browser and after 2-3 seconds close tab (even if you don't see anything on screen), wait a little longer and check if file is created. It's working on my linux machine.

piotrekkr
  • 2,785
  • 2
  • 21
  • 35
  • Then why should i need to use the header function ?? User need to feel that the connection is closed and he can proceed to another thing,also this is a private URL and it can only be accessed through one of our application. – Red Apr 30 '13 at 08:33
  • Showing information "connection closed go do things" in the middle of script can be highly server dependent. There are factors like server configuration or output compression or even browser can buffer output from script. So showing it before end of script can be really hard to impossible. Read here: http://pl1.php.net/manual/en/function.flush.php – piotrekkr Apr 30 '13 at 09:00
2

Because of this cool possibility, which Red posted, I've written a small utility class which provides a queue where you can add Closures for later execution:

<?php

namespace Company\Project\Utilities;

/**
 * Class ContinueUtility
 *
 * @package Company\Project\Utilities
 */
class ContinueUtility {
/**
 * Stored tasks
 * @var array
 */
static protected $tasks = array();

/** Constant for new line in HTTP Header */
const HEADER_NEW_LINE = "\r\n";

/**
 * Add task (closure/function) to queue, with set arguments
 *
 * @param \Closure $task
 * @param array $arguments
 * @return void
 */
public static function addTask(\Closure $task, array $arguments = array()) {
    self::$tasks[] = array(
        'closure' => $task,
        'arguments' => $arguments
    );
}

/**
 * Returns TRUE if tasks has been set, otherwise FALSE
 *
 * @return boolean
 */
public static function hasTasks() {
    return !empty(self::$tasks);
}

/**
 * Clear all previous set tasks
 *
 * @return void
 */
protected static function clearTasks() {
    self::$tasks = array();
}

/**
 * Execute all previous set tasks
 *
 * @return void
 */
protected static function executeTasks() {
    foreach (self::$tasks as $task) {
        call_user_func_array($task['closure'], $task['arguments']);
    }
}

/**
 * Execute and clear all previous set tasks
 *
 * @return void
 */
public static function executeAndClearTasks() {
    self::executeTasks();
    self::clearTasks();
}

/**
 * Closes the HTTP connection to client immediately and outputs given string.
 *
 * @param string $instantOutput
 * @return void
 */
public static function closeConnection($instantOutput = '') {
    set_time_limit(0);
    ignore_user_abort(TRUE);
    header('Connection: close' . self::HEADER_NEW_LINE);
    header('Content-Encoding: none' . self::HEADER_NEW_LINE);
    ob_start();
    echo $instantOutput;
    $size = ob_get_length();
    header('Content-Length: ' . $size, TRUE);
    ob_end_flush();
    ob_flush();
    flush();
}
}

This is how you add new tasks to queue:

use Company\Project\Utilities\ContinueUtility;

$a = 4;
$b = 5;
ContinueUtility::addTask(function($a, $b){
    sleep(5);
    $c = a + b;
    file_put_contents(__DIR__ . '/whatever.log', $a . '+' . $b . '=' . $c);
}, array(
    $a, $b
));

And this is how you trigger execution of all previous added tasks:

ContinueUtility::closeConnection('Ready.');
ContinueUtility::executeAndClearTasks();
Armin
  • 15,582
  • 10
  • 47
  • 64
  • It works perfectly on a project where I'm using it currently. I send push notifications to smartphones and however for APNS (apple) it takes sometimes 30 seconds until the message is out. That means, that the HTTP request will not be completed until these 30 seconds. Now the request is instantly done and the notification will be send in the background :) – Armin Nov 12 '13 at 08:24
0

If you are using PHP-FPM, a cleaner and more versatile solution would be to simply execute fastcgi_finish_request();

From PHP.net's documentation

This function flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open.

This is how Symfony handles its onTerminate.