12

My intention is this.

My client.html calls a php script check.php via ajax. I want check.php to check if another script task.php is already being run. If it is, I do nothing. If it is not, I need to run it in the background.

I have an idea what I want to do, but am unsure how to do it.

Part A. I know how to call check.php via ajax.

Part B. In check.php I might need to run task.php. I think I need something like:

$PID = shell_exec("php task.php > /dev/null & echo $!");

I think the "> /dev/null &" bit tells it to run in the background, but am unsure what the "$!" does.

Part C. The $PID I need as a tag of the process. I need to write this number (or whatever) to a file in the same directory, and need to read it every call to check.php. I can't work out how to do that. Could someone give me a link of how to read/write a file with a single number in to the same directory?

Part D. Then to check if the last launched task.php is still running I am going to use the function:

function is_process_running($PID)
{
   exec("ps $PID", $ProcessState);
   return(count($ProcessState) >= 2);
}

I think that is all the bits I need, but as you can see I am unsure on how to do a few of them.

2

6 Answers 6

15
+100

I would use an flock() based mechanism to make sure that task.php runs only once.

Use a code like this:

<?php

$fd = fopen('lock.file', 'w+');

// try to get an exclusive lock. LOCK_NB let the operation not blocking
// if a process instance is already running. In this case, the else 
// block will being entered.
if(flock($fd, LOCK_EX | LOCK_NB )) {
    // run your code
    sleep(10);
    // ...
    flock($fd, LOCK_UN);
} else {
    echo 'already running';
}

fclose($fd);

Also note that flock() is, as the PHP documentation points out, portable across all supported operating systems.


!$

gives you the pid of the last executed program in bash. Like this:

command &
pid=$!
echo pid

Note that you will have to make sure your php code runs on a system with bash support. (Not windows)


Update (after comment of opener).

flock() will work on all operating systems (As I mentioned). The problem I see in your code when working with windows is the !$ (As I mentioned ;) ..

To obtain the pid of the task.php you should use proc_open() to start task.php. I've prepared two example scripts:

task.php

$fd = fopen('lock.file', 'w+');

// try to get an exclusive lock. LOCK_NB let the operation not blocking
// if a process instance is already running. In this case, the else 
// block will being entered.
if(flock($fd, LOCK_EX | LOCK_NB )) {
    // your task's code comes here
    sleep(10);
    // ...
    flock($fd, LOCK_UN);
    echo 'success';
    $exitcode = 0;
} else {
    echo 'already running';
    // return 2 to let check.php know about that
    // task.php is already running
    $exitcode = 2; 
}

fclose($fd);

exit($exitcode);

check.php

$cmd = 'php task.php';
$descriptorspec = array(
   0 => array('pipe', 'r'),  // STDIN 
   1 => array('pipe', 'w'),  // STDOUT
   2 => array('pipe', 'w')   // STDERR
);

$pipes = array(); // will be set by proc_open()

// start task.php
$process = proc_open($cmd, $descriptorspec, $pipes);

if(!is_resource($process)) {
    die('failed to start task.php');
}

// get output (stdout and stderr)
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);

do {
    // get the pid of the child process and it's exit code
    $status = proc_get_status($process);
} while($status['running'] !== FALSE);

// close the process
proc_close($process);

// get pid and exitcode
$pid = $status['pid'];
$exitcode = $status['exitcode'];

// handle exit code
switch($exitcode) {
    case 0:
        echo 'Task.php has been executed with PID: ' . $pid
           . '. The output was: ' . $output;
        break;
    case 1:
        echo 'Task.php has been executed with errors: ' . $output;
        break;
    case 2:
        echo 'Cannot execute task.php. Another instance is running';
        break;
    default:
        echo 'Unknown error: ' . $stdout;
}

You asked me why my flock() solution is the best. It's just because the other answer will not reliably make sure that task.php runs once. This is because the race condition I've mentioned in the comments below that answer.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for your answers guys. I like this idea of locking files, but I don't know enough about this to know which is the best method. Can you agree, or give a layman reason why yours is the best? I may be using a windows server, I may be using a linux one. So the method needs to work on both.
Excellent Answer Sir. I am just little bit confused at one point. Please tell what is the purpose of $exitcode in task.php because this is making no difference even I remove it from task.php. And also why did you use exit($exitcode); in task.php in the end while the script is already getting end.
oftentimes you can just $fp=fopen(__FILE__,'rb');flock($fp , without the need for a dedicated lock file. but +1, flock() definitely is the answer!
@AbdulJabbarWebBestow For you only the first part of my answer is interesting. The first code example. The remaining part is special to the original question.
5

You can realize it, using lock file:

if(is_file(__DIR__.'/work.lock'))
{
    die('Script already run.');
}
else
{
    file_put_contents(__DIR__.'/work.lock', '');
    // YOUR CODE
    unlink(__DIR__.'/work.lock');
}

7 Comments

Do you have ever heard about race conditions?
This is not the answer to the question ;)
read wikipedia article, do you mean i missunderstand your issue?
The race condition in your code: Process A asks: is_file(). Got answer: No. then went off from CPU. Process B get into CPU and asks is_file(). Got answer: No. --> Perfect disaster, both processes will enter the else block. Although upvoted I would say this is what is called a bad advice
In this situation, you need code this task in other language. IMHO
|
2

Too bad I didn't see this before it was accepted..

I have written a class to do just this. ( using file locking ) and PID, process ID checking, on both windows and Linux.

https://github.com/ArtisticPhoenix/MISC/blob/master/ProcLock.php

Comments

1

I think your are really overdoing it with all the processes and background checks. If you run a PHP script without a session, then you are already essentially running it in the background. Because it will not block any other request from the user. So make sure you don't call session_start();

Then the next step would be to run it even when the user cancels the request, which is a basic function in PHP. ignore_user_abort

Last check is to make sure it's only runs once, which can be easily done with creating a file, since PHP doesnt have an easy application scope.

Combined:

<?php
// Ignore user aborts and allow the script
// to run forever
ignore_user_abort(true);
set_time_limit(0);

$checkfile = "./runningtask.tmp";

//LOCK_EX basicaly uses flock() to prevents racecondition in regards to a regular file open.
if(file_put_contents($checkfile, "running", LOCK_EX)===false) {
    exit();
}

function Cleanup() {
  global $checkfile;
  unlink($checkfile);
}


/*
actual code for task.php    
*/


//run cleanup when your done, make sure you also call it if you exit the code anywhere else
Cleanup();
?>

In your javascript you can now call the task.php directly and cancel the request when the connection to the server has been established.

<script>
function Request(url){
  if (window.XMLHttpRequest) { // Mozilla, Safari, ...
      httpRequest = new XMLHttpRequest();
  } else if (window.ActiveXObject) { // IE
      httpRequest = new ActiveXObject("Microsoft.XMLHTTP");
  } else{
      return false;
  }
  httpRequest.onreadystatechange = function(){
      if (httpRequest.readyState == 1) {
        //task started, exit
        httpRequest.abort();
      }
  };
  httpRequest.open('GET', url, true);
  httpRequest.send(null);
}

//call Request("task.php"); whenever you want.
</script>

Bonus points: You can have the actual code for task.php write occasional updates to $checkfile to have a sense of what is going on. Then you can have another ajax file read the content and show the status to the user.

3 Comments

What if the actual code for task.php block crashes before it gets to Cleanup()? You're then left with a runningtask.tmp lockfile that never gets deleted.
The same thing goes for the flock() method when unlock is never called. But you could update the file with the timestamp every 10seconds or so and if the timestamp is more then 20seconds old, restart the task
True, but this assumes task.php has the ability to update the lockfile at regular intervals. If it's doing something like performing a blocking MySQL query that takes an indeterminate amount of time, this won't work.
1

Lets make the whole process from B to D simple

Step B-D:

$rslt =array(); // output from first exec
$output = array(); // output of task.php execution

//Check if any process by the name 'task.php' is running
exec("ps -auxf | grep 'task.php' | grep -v 'grep'",$rslt);

if(count($rslt)==0) // if none,
  exec('php task.php',$output); // run the task,

Explanation:

ps -auxf        --> gets all running processes with details 
grep 'task.php' --> filter the process by 'task.php' keyword
grep -v 'grep'  --> filters the grep process out

NB:

  1. Its also advisable to put the same check in task.php file.

  2. If task.php is executed directly via httpd (webserver), it will only be displayed as a httpd process and cannot be identified by 'ps' command

  3. It wouldn't work under load-balanced environment. [Edited: 17Jul17]

Comments

1

You can get an exclusive lock on the script itself for the duration of the script running

Any other attempts to run it will end as soon as the lock() function is invoked.

//try to set a global exclusive lock on the file invoking this function and die if not successful
function lock(){
  $file = isset($_SERVER['SCRIPT_FILENAME'])?
    realpath($_SERVER['SCRIPT_FILENAME']):
    (isset($_SERVER['PHP_SELF'])?realpath($_SERVER['PHP_SELF']):false);
  if($file && file_exists($file)){
    //global handle stays alive for the duration if this script running
    global $$file;
    if(!isset($$file)){$$file = fopen($file,'r');}
    if(!flock($$file, LOCK_EX|LOCK_NB)){
        echo 'This script is already running.'."\n";
        die;
    }
  }
}

Test

Run this in one shell and try to run it in another while it is waiting for input.

lock();

//this will pause execution until an you press enter
echo '...continue? [enter]';
$handle = fopen("php://stdin","r");
$line = fgets($handle);
fclose($handle);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.