0

I have one PHP script that can take several minutes to complete. The script downloads a file to the user PC.

I have another PHP script and its role is to monitor progress of the main download script. That script is called by the client via AJAX calls and should return download progress information.

Right now, my tests show, that during the execution of the main script(in other words, during file download), the AJAX - monitor script returns no values at all. It starts behaving normally, when the main - Download script finishes.

Is it possible that PHP can not run two or more scripts simultaneously and it allows to run script only in sequential order?

I could insert my code, but I think for the purpose of my question, it is not needed. I simply need to know, if two or more PHP scripts may run simultaneously for the same client.

I use:

  • WAMP
  • PHP Version 5.4.12
  • JavaScript without jQuery

Code Used:

As I was asked to show you my code, please, see the below code parts.

Main PHP(later Download) Script:

<?php    
    // disable script expiry
    set_time_limit(0);

// start session if session is not already started
if (session_status() !== PHP_SESSION_ACTIVE)
{
    session_start();
}    

// prepare session variable
$_SESSION['DownloadProgress'] = 0;    

for( $count = 0; $count < 60; $count++)
{
    sleep(1);

    echo "Iteration No: " . $count;

    $_SESSION['DownloadProgress']++;
    echo '$_SESSION[\'DownloadProgress\'] = ' . $_SESSION['DownloadProgress'];
    flush();
    ob_flush();
}
?>

Monitoring PHP script:

// construct JSON
$array = array("result" => 1, "download_progress" => $_SESSION['DownloadProgress']);
echo json_encode($array);
?>

JavaScript code, where I call the both PHP scripts:

   SearchResults.myDownloadFunction = function()
    {
        console.log( "Calling: PHP/fileDownload.php" );
        window.location.href = 'PHP/fileDownload.php?upload_id=1';


        console.log( "Calling: getUploadStatus()" );
        FileResort.SearchResults.getUploadStatus();

        console.log( "Called both functions" );   
    };

JavaScript AJAX:

// call AJAX function to get upload status from the server
SearchResults.getUploadStatus = function ()
{

    var SearchResultsXMLHttpRequest = FileResort.Utils.createRequest();

    if (SearchResultsXMLHttpRequest == null)
    {
        console.log("unable to create request object.");
    }
    else
    {
        SearchResultsXMLHttpRequest.onreadystatechange = function ()
        {
            console.log("Response Text: " + SearchResultsXMLHttpRequest.responseText);
            console.log("AJAX Call Returned");

            if ((SearchResultsXMLHttpRequest.readyState == 4) && (SearchResultsXMLHttpRequest.status == 200))
            {
                //if (that.responseJSON.result == "true")
                {
                    var responseJSON = eval('(' + SearchResultsXMLHttpRequest.responseText + ')');
                    console.log("Download Progress: " + responseJSON.download_progress);
                }
            }
        }

        var url = "PHP/fileDownloadStatus.php";
        SearchResultsXMLHttpRequest.open("POST", url, true);
        SearchResultsXMLHttpRequest.send();
    }
};

Code Update I:

PHP Script that will later download files:

<?php
// disable script expiry
set_time_limit(0);

for( $count = 0; $count < 60; $count++)
{
    sleep(1);
}
?>

PHP Monitoring script that outputs test values:

<?php
$test_value = 25;

// construct JSON
$array = array("result" => 1, "download_progress" => $test_value);

//session_write_close();
echo json_encode($array);
?>

Both scripts are called followingly:

SearchResults.myDownloadFunction = function()
{
    console.log( "Calling: PHP/fileDownload.php" );
    window.setTimeout(FileResort.SearchResults.fileDownload(), 3000);


    console.log( "Calling: getUploadStatus()" );
    window.setInterval(function(){FileResort.SearchResults.getDownloadStatus()}, 1000);

    console.log( "Called both functions" );    
};
Bunkai.Satori
  • 4,698
  • 13
  • 49
  • 77
  • 1
    I think you _do_ need to include some code. How are the AJAX calls being issued? Your problem could be on the server, or it could be on the client. Are you seeing log messages for server requests? etc. etc. – Mike Edwards Oct 29 '13 at 19:09
  • You definitely need to include code, because otherwise the answer to your question is just "Yes." Multiple PHP scripts can absolutely run simultaneously on the same client. (Otherwise Facebook would be in big trouble.) Whether each PHP process can get insight into what is happening in other PHP processes is a different question. – glomad Oct 29 '13 at 19:17
  • @MikeEdwards, please, see the updated questions with all the important code included. – Bunkai.Satori Oct 29 '13 at 19:36
  • @ithcy, basically your answer is critical for me to understand, that problem is within my code and not in the PHP architecture. It is important to know, that for one session, multiple scripts can be run simultaneously. I would say, it could be realistic that scripts could run only sequentially within a session. However, if any error is found in my samples, that would help a lot. thanks in advance. – Bunkai.Satori Oct 29 '13 at 19:39
  • @Bunkai.Satori Well, your problem may still be in the PHP architecture, just not in that particular way :) – glomad Oct 29 '13 at 19:45
  • @ithcy, I did not write and close the session. Maybe that will be my problem. I will test it now. – Bunkai.Satori Oct 29 '13 at 19:46
  • 1
    I just posted a working example using sessions that should work for you. – Ben D Oct 31 '13 at 04:23

1 Answers1

6

Without more info there are a few possibilities here, but I suspect that the issue is your session. When a script that uses the session file start, PHP will lock the session file until session_write_close() is called or the script completes. While the session is locked any other files that access the session will be unable to do anything until the first script is done and writes/closes the session file (so the ajax calls have to wait until the session file is released). Try writing the session as soon as you've done validation, etc on the first script and subsequent scripts should be able to start.

Here's a quick and dirty approach:

The "Landing" page:

This is the page that the user is going to click the download link

<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$(document).ready(function(e) {
    //Every 500ms check monitoring script to see what the progress is
    $('#large_file_link').click(function(){
        window.p_progress_checker  = setInterval( function(){
            $.get( "monitor.php", function( data ) {
                $( ".download_status" ).html( data +'% complete' );
                //we it's done or aborted we stop the interval
                if (parseInt(data) >= 100 || data=='ABORTED'){
                    clearInterval(window.p_progress_checker);
                }
                //if it's aborted we display that
                if (data=='ABORTED'){
                    $( ".download_status" ).html( data );
                    $( ".download_status" ).css('color','red').css('font-weight','bold');
                }
            })
        }, 500);
    });
});
</script>
</head>
<body>
    <div class="download_status"><!-- GETS POPULATED BY AJAX CALL --></div>
    <p><a href="download.php" id="large_file_link">Start downloading large file</a></p>
</body>
</html>

The "File Uploader"

This is the PHP script that serves the large file... it breaks it into chunks and after sending each chunk it closes the session so the session becomes available to other scripts. Also notice that I've added a ignore_user_abort/connection_aborted handler so that it can take a special action should the connection be terminated. This is the section that actually deals with the session_write_close() issue, so focus on this script.

<?php
/*Ignore user abort so we can catch it with connection_aborted*/
ignore_user_abort(true);

function send_file_to_user($filename) {

   //Set the appropriate headers:
    header('Content-Description: File Transfer');
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename='.basename($filename));
    header('Content-Transfer-Encoding: binary');
    header('Expires: 0');
    header('Cache-Control: must-revalidate');
    header('Pragma: public');
    header('Content-Length: ' . filesize($filename));


   $chunksize = 10*(1024); // how many bytes per chunk (i.e. 10K per chunk)
   $buffer = '';
   $already_transferred =0;
   $file_size = filesize( $filename );
   $handle = fopen($filename, 'rb');
   if ($handle === false) {
       return false;
   }
   while (!feof($handle)) {
      /*if we're using a session variable to commnicate just open the session 
        when sending a chunk and then close the session again so that other 
        scripts which have request the session are able to access it*/ 
      session_start();

      //see if the user has aborted the connection, if so, set the status
      if (connection_aborted()) { 
            $_SESSION['file_progress'] = "ABORTED";
            return;
       }

       //otherwise send the next packet...
       $buffer = fread($handle, $chunksize);
       echo $buffer;
       ob_flush();
       flush();

       //now update the session variable with our progress
       $already_transferred += strlen($buffer);
       $percent_complete = round( ($already_transferred / $file_size) * 100);
       $_SESSION['file_progress'] = $percent_complete;

       /*now close the session again so any scripts which need the session
         can use it before the next chunk is sent*/
       session_write_close();
   }
   $status = fclose($handle);
   return $status;
} 

send_file_to_user( 'large_example_file.pdf');
?>

The "File Monitor"

This is a script that is called via Ajax and is in charge of reporting progress back to the Landing Page.

<?
session_start();
echo $_SESSION['file_progress'];
?>
Ben D
  • 14,321
  • 3
  • 45
  • 59
  • I did not write session at all. I will try it and see what happens. I understood, that you need to see my code, I therefore additionally included some samples of my code. Thank you very much for that tip. – Bunkai.Satori Oct 29 '13 at 19:45
  • 1
    Both scripts you posted are accessing the session file (they're using a session variable to communicate with one another). You could either communicate via another medium (db or alternate temp file) or you could try closing you could try `session_write_close()` right before you `sleep(1)` and then resume the session immediately after the sleep. It's not a perfect approach but it should work – Ben D Oct 29 '13 at 19:53
  • if I understand correctly, if I want to use session variables, I have to call `session_start()` and `session_write_close()` within every script? Hmm.. the examples from [www.php.net](http://www.php.net/manual/en/function.session-start.php) do not use `session_write_close()` and assume that the session variables will be shared between scripts. I tried it, but so far no progress. – Bunkai.Satori Oct 29 '13 at 20:08
  • 1
    Normally there's no reason to call session_write_close because it's invoked automatically as part of the cleanup process when the script completes. However, in situations where scripts run for long periods of time (pretty much any time you're invoking `sleep`, for instance) you'll want to manually close the session to make it available to other scripts and then re-open the session when you need it again (see: http://stackoverflow.com/questions/12315225/reopening-a-session-in-php) – Ben D Oct 29 '13 at 20:16
  • it makes perfect sense. Do I have to start and write the session, if, in the script, I access session variables for reading only? – Bunkai.Satori Oct 29 '13 at 20:25
  • I simplified my code as much as possible. In regular intervals I start calling the AJAX monitoring PHP script. Then, 3 seconds later, I call the main download script and let it running for 60 seconds. I removed anyhing session related from the main - download script. Session variable is incremented only in the AJAX script. Interesting is, that before starting the main download script, the AJAX script runs properly. Once the download script starts, AJAX stops returning any values. – Bunkai.Satori Oct 29 '13 at 20:44
  • Sorry for the delay there. Can you post an updated version of your download script? – Ben D Oct 29 '13 at 22:00
  • 1
    let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/40224/discussion-between-ben-d-and-bunkai-satori) – Ben D Oct 29 '13 at 22:11
  • Unfortunately, this problem still persists. I tried to use two ajax functions, but problem is, that AJAX calls can not be used for file download. If anyone has an idea, that would really help. I can provide you with sample source code. – Bunkai.Satori Oct 30 '13 at 12:38
  • 1
    Did you see my last entry in chat? Try downloading these files: md-hq.com/sandbox/async.zip. Set permissions on status.txt to 777 and try uploading a file through index.php. – Ben D Oct 30 '13 at 18:51
  • Thanks for the note. I found it now. I have downloaded it. It looks to me, that we implemented file Upload, and not Download, which is what I am looking for. Please see the chat and my [other question here](http://stackoverflow.com/questions/19692282/php-script-in-iframe-blocks-other-code). – Bunkai.Satori Oct 30 '13 at 20:04
  • hi and I have to tell you, great job. However, I believe it would be more elegant to store progress information in Session variables and pull it with another AJAX calls. Would this work in your environment? In my one it does not work. Please, see my message in the chat window. – Bunkai.Satori Oct 31 '13 at 01:35
  • you helped me a lot and this solution is exactly I was looking for. Thank you and let me mark this as the accepted answer and give you some additional points for useful comments. Thanks Ben! – Bunkai.Satori Oct 31 '13 at 12:07