<?php
/*
 * $RCSfile: FlockLockSystem.class,v $
 *
 * Gallery - a web based photo album viewer and editor
 * Copyright (C) 2000-2005 Bharat Mediratta
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
 */
/**
 * @version $Revision: 1.9 $ $Date: 2005/08/23 03:49:02 $
 * @package GalleryCore
 * @author Bharat Mediratta <bharat@menalto.com>
 */

GalleryCoreApi::relativeRequireOnce('modules/core/classes/GalleryLockSystem.class');

/**
 * Flock() based locking.  This is fairly efficient, but it will not work on NFS
 * and is known to be unreliable on some operating systems including some
 * flavors of the 2.4 Linux kernel.
 *
 * @package GalleryCore
 * @subpackage Classes
 * @abstract
 */
class FlockLockSystem extends GalleryLockSystem {

    /**
     * Information about all the locks we currently hold
     */
    var $_locks;

    /**
     * Reference counts for every lock we're holding so that if we've got a file doubly read locked
     * we don't try to delete it until all read locks are released.
     *
     * TODO: When we get rid of double read locks, we can delete this.
     */
    var $_references;

    /**
     * Constructor
     */
    function FlockLockSystem() {
	$this->_locks = array();
    }

    /**
     * @see GalleryLockSystem::acquireReadLock()
     */
    function acquireReadLock($ids, $timeout=10) {
	if (!is_array($ids)) {
	    $ids = array($ids);
	}

	list ($ret, $lockId) = $this->_acquireLock($ids, $timeout, LOCK_READ);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(), $lockId);
    }

    /**
     * @see GalleryLockSystem::isReadLocked()
     */
    function isReadLocked($id) {
	foreach ($this->_locks as $lockId => $lock) {
	    if (in_array($id, $lock['ids'])) {
		return true;
	    }
	}

	return false;
    }

    /**
     * @see GalleryLockSystem::acquireWriteLock()
     */
    function acquireWriteLock($ids, $timeout=10) {
	if (!is_array($ids)) {
	    $ids = array($ids);
	}

	list ($ret, $lockId) = $this->_acquireLock($ids, $timeout, LOCK_WRITE);
	if ($ret->isError()) {
	    return array($ret->wrap(__FILE__, __LINE__), null);
	}

	return array(GalleryStatus::success(), $lockId);
    }

    /**
     * @see GalleryLockSystem::isWriteLocked()
     */
    function isWriteLocked($id) {
	foreach ($this->_locks as $lockId => $lock) {
	    if ($lock['type'] == LOCK_WRITE && in_array($id, $lock['ids'])) {
		return true;
	    }
	}

	return false;
    }

    /**
     * @see GalleryLockSystem::releaseLocks()
     */
    function releaseLocks($lockIds) {
	global $gallery;

	if (!is_array($lockIds)) {
	    $lockIds = array($lockIds);
	}

	/* Release all locks by closing the files */
	$platform = $gallery->getPlatform();
	$gallery->guaranteeTimeLimit(count($lockIds));
	foreach ($lockIds as $lockId) {
	    if (empty($lockId)) {
		continue;
	    }

	    foreach ($this->_locks[$lockId]['lockInfo'] as $lockInfo) {
		list ($fd, $lockFile) = $lockInfo;
		$platform->fclose($fd);
		if ($platform->file_exists($lockFile)) {
		    $this->_references[$lockFile]--;
		    if ($this->_references[$lockFile] == 0) {
			$platform->unlink($lockFile);
		    }
		}
	    }
	    unset($this->_locks[$lockId]);
	}

	return GalleryStatus::success();
    }

    /**
     * @see GalleryLockSystem::refreshAllLocks()
     */
    function releaseAllLocks() {
	global $gallery;

	$platform = $gallery->getPlatform();
	$gallery->guaranteeTimeLimit(count($this->_locks));
	foreach ($this->_locks as $lockId => $lock) {
	    foreach ($lock['lockInfo'] as $lockInfo) {
		list ($fd, $lockFile) = $lockInfo;
		$platform->fclose($fd);
		if ($platform->file_exists($lockFile)) {
		    $this->_references[$lockFile]--;
		    if ($this->_references[$lockFile] == 0) {
			$platform->unlink($lockFile);
		    }
		}
	    }
	    unset($this->_locks[$lockId]);
	}

	return GalleryStatus::success();
    }

    /**
     * @see GalleryLockSystem::refreshLocks()
     */
    function refreshLocks($freshUntil) {
	global $gallery;

	/* Flush one byte to each lock file to update its timestamp */
	$platform = $gallery->getPlatform();

	foreach ($this->_locks as $lockId => $lock) {
	    foreach ($lock['lockInfo'] as $lockInfo) {
		list ($fd, $lockFile) = $lockInfo;
		$count = $platform->fwrite($fd, '.');
		if ($count == 0) {
		    return GalleryStatus::error(ERROR_STORAGE_FAILURE, __FILE__, __LINE__,
						$lockFile);
		}
		$platform->fflush($fd);
	    }
	}

	return GalleryStatus::success();
    }

    /**
     * Return the lock file for a given entity id
     *
     * @param int the input id
     * @return string the complete path to the lock file
     */
    function _getLockFile($id) {
	global $gallery;

	$locksDir = $gallery->getConfig('data.gallery.locks');
	$tmp = "$id";
	if ($id < 100) {
	    $first = '0';
	    $second = $tmp[0];
	} else {
	    $first = $tmp[0];
	    $second = $tmp[1];
	}

	return "$locksDir$first/$second/$id";
    }

    /**
     * Read Lock one or more objects
     *
     * @param array ids to lock
     * @param integer how long to wait for the lock before giving up
     * @param the type of lock (LOCK_READ, LOCK_WRITE)
     * @return array object GalleryStatus a status code
     *               int the lock id
     * @access private
     * @static
     */
    function _acquireLock($ids, $timeout, $lockType) {
	global $gallery;
	$cutoffTime = time() + $timeout;
	$platform = $gallery->getPlatform();

	/* Get a file handle (which is actually a lock handle) for all the files first */
	$notLocked = array();
	foreach ($ids as $id) {
	    $lockFile = $this->_getLockFile($id);
	    $fd = $platform->fopen($lockFile, 'wb+');
	    if ($fd) {
		$notLocked[] = array($fd, $lockFile);
	    } else {
		/* Close the files that we already opened successfully and return an error */
		foreach ($notLocked as $lockInfo) {
		    list ($fd, $lockFile) = $lockInfo;
		    $platform->fclose($fd);
		    /*
		     * Delete the lock files even if others are locking it too (releaseLock calls
		     * are too often forgotten in error handling
		     */
		    if ($platform->file_exists($lockFile)) {
			@$platform->unlink($lockFile);
		    }
		}
		return array(GalleryStatus::error(ERROR_PLATFORM_FAILURE, __FILE__, __LINE__,
						  $lockFile), null);
	    }
	}

	/* Move them from notLocked -> locked as we acquire the lock */
	$locked = array();
	$wouldBlock = null;
	while (!empty($notLocked)) {
	    $tmp = $notLocked;
	    $notLocked = array();
	    foreach ($tmp as $lockInfo) {
		list ($fd, $lockFile) = $lockInfo;
		if ($lockType == LOCK_READ) {
		    $flockType = LOCK_SH;
		} else {
		    $flockType = LOCK_EX;
		}
		/* Check if we can lock */
		$flockReturned = $platform->flock($fd, $flockType | LOCK_NB, $wouldBlock);
		if ($flockReturned && !$wouldBlock) {
		    $locked[] = $lockInfo;
		    /* Keep track of the number of locks there are for this entity */
		    if (isset($this->_references[$lockFile])) {
			$this->_references[$lockFile]++;
		    } else {
			$this->_references[$lockFile] = 1;
		    }
		} else {
		    /* Remember that it's not locked and keep going */
		    $notLocked[] = $lockInfo;
		}
	    }

	    if (!empty($notLocked)) {
		if (time() > $cutoffTime) {
		    /* Couldn't get the locks in time, release the ones that we have and return */
		    foreach (array_merge($locked, $notLocked) as $lockInfo) {
			list ($fd, $lockFile) = $lockInfo;
			$platform->fclose($fd);
			if ($platform->file_exists($lockFile)) {
			    $this->_references[$lockFile]--;
			    if ($this->_references[$lockFile] == 0) {
				$platform->unlink($lockFile);
			    }
			}
		    }
		    return array(GalleryStatus::error(ERROR_LOCK_TIMEOUT, __FILE__, __LINE__,
			array_reduce($notLocked,
			    create_function('$v,$w', 'return empty($v) ? $w[1] : "$v $w[1]";'))),
			null);
		}

		/* Wait a second and try any unacquired locks again */
		$gallery->debug('Waiting for a lock');
		sleep(1);
	    }
	}

	$lockId = md5(microtime());
	$this->_locks[$lockId] = array('type' => $lockType,
				       'ids' => $ids,
				       'lockInfo' => $locked);
	return array(GalleryStatus::success(), $lockId);
    }


    /**
     * Return the ids of all the locks we hold
     *
     * @return array lock ids
     */
    function getLockIds() {
	return array_keys($this->_locks);
    }
}
?>
