1
\$\begingroup\$

We had a situation where it was necessary that a script only be running in one process at a time.

The solution was to make a "LockByProcess" class that checks a database for an existing process, and kill it if requested, or prevent the new process from starting if not.

Is there a better way to do this without involving a database? The problem is this script is often run by apache, so you can't see it in the process list generally. And we want to be run by launchdaemon most of the time, but to be run (and interruptible) by going to a web page to run the same.

Here is the line at the top of any script you want to limit:

$interrupt = ( @$_REQUEST['interrupt'] == 1 ? 1 : 0 );

$lock = new LockByProcess( __FILE__, getmypid(), $interrupt );

And the class itself (forgive the mysql class, I am stuck with using a legacy db connection script at the moment).

<?php

/**
 * This prevents a script from running in more than one process.
 *
 * Call it at the begining of any script with a name common to the script (preferrably the script name itself, using the __FILE__ constant), the current process id, and an
 * "interrupt" flag (which determines wether to interrupt a running instance of the script for the current one) and the script will run, or not, based on the situation.
 *
 * In no case will more than one instance of the script be allowed to run at once. If the script is
 * interruptable, the LockByProcess class will kill the current running process. If not, a new process
 * will not be allowed to begin.
 */
require_once("incl-mysql.php"); 

class LockByProcess
{   
    public  $db;

    private $name;
    private $pid;
    private $interrupt;

    private $logfile;

    function __construct( $name, $pid, $interupt )
    {
        $this->db = db("locks");

        $this->name      = $name;
        $this->pid       = $pid;
        $this->interrupt = $interupt;       

        /**
         * check if a lock is already set
         */
        $sql = "SELECT
                    lock_name,
                    lock_pid
                FROM locks
                WHERE
                    lock_name  = '".mysql_real_escape_string($this->name,$this->db)."' AND
                    lock_pid  != '".mysql_real_escape_string($this->pid,$this->db)."'";
        $result = mysql_query($sql,$this->db) or die("db locks error: ".mysql_error($this->db));
        $current_lock = mysql_fetch_assoc($result);

        /**
         * set up cases:
         * have_lock_interrupt,
         * have_lock_donot_interrupt,
         * no_lock
         */
        $situation = NULL;
        if($current_lock){
            if($this->interrupt == 1) $situation = 'have_lock_interrupt';
            else                      $situation = 'have_lock_donot_interrupt';
        }else $situation = 'no_lock';

        /**
         * run functions for correct situation
         */
        switch($situation){
            case 'have_lock_interrupt':
                $this->lockExistsInterrupt($current_lock);
                break;
            case 'have_lock_donot_interrupt':
                $this->lockExistsNoInterrupt();
                break;
            case 'no_lock':
                $this->setLock();
                break;
        }
    }

    /**
     * A lock exists, but we will kill that process an start our own
     */
    function lockExistsInterrupt($current_lock){
        // kill current process
        exec("kill {$current_lock['lock_pid']}");
        // replace old lock with new lock
        $sql = "
            REPLACE INTO locks (
                lock_name,
                lock_pid
            )VALUES(
                '".mysql_real_escape_string($this->name,$this->db)."',
                '".mysql_real_escape_string($this->pid,$this->db)."'
            );";
        mysql_query($sql,$this->db) or die("db locks error: ".mysql_error($this->db));  
    }

    function lockExistsNoInterrupt(){
        die('If you want to interrupt the current lock, you have to set the interrupt parameter.');
    }

    function setLock(){
        $sql = "
            INSERT INTO locks ( 
                lock_name, 
                lock_pid 
            )VALUES(
                '".mysql_real_escape_string($this->name,$this->db)."',
                '".mysql_real_escape_string($this->pid,$this->db)."'
            );";
        mysql_query($sql,$this->db) or die("db locks error: ".mysql_error($this->db));
    }

    /**
     * remove the lock from the database completely
     */
    public function unlock()
    {
        $sql = "DELETE 
                    FROM locks 
                WHERE 
                    lock_name = '".mysql_real_escape_string($this->name,$this->db)."' AND 
                    lock_pid  = '".mysql_real_escape_string($this->pid,$this->db)."'";
        mysql_query($sql,$this->db) or die("db locks error: ".mysql_error($this->db));
    }

    function __destruct(){
        $this->unlock();
    }
}
\$\endgroup\$
  • \$\begingroup\$ You can use flock() - Portable advisory file locking. \$\endgroup\$ – Marek Jun 30 '14 at 14:57
  • \$\begingroup\$ find all the running processes using a shell command: exec(sprintf('ps -A | grep %s', __FILE__), $out, $status); process the $out array and handle situation accordingly. That's a quick 'n dirty way. A better way would be to use proc_open, and use pipes, and an even better way would be to seriously consider using something else than PHP. Also: replacing legacy mysql_* code is not acceptable. For a class as simple as this, switching to the non-deprecated extensions is not a big task, so do it already! \$\endgroup\$ – Elias Van Ootegem Jun 30 '14 at 15:15
  • \$\begingroup\$ One thing about what you describe is, as I said, I often can't see the name in the process list because sometimes it runs as apache. As for the mysql class, you are absolutely correct, thanks for the notes. \$\endgroup\$ – dgig Jun 30 '14 at 15:58
  • \$\begingroup\$ I found this which had some helpful hints, and with this looking especially promising. \$\endgroup\$ – dgig Jun 30 '14 at 16:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.