Ellrion
9/14/2016 - 12:36 PM

Консольная команда с блокировкой.

Консольная команда с блокировкой.

<?php 

namespace App\Console;

use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class BaseCommand extends Command
{
    /**
     * Обычный запуск команды.
     * Возможно конкурентное выполнение команды.
     */
    const RUN_SIMPLE = 0;

    /**
     * Эксклюзивный запуск команды.
     * Выполняется только один экземпляр команды.
     */
    const RUN_BLOCKING = 1;

    /**
     * Организует очередь экземпляров команд.
     * Выполняется только один экземпляр команды,
     * каждый последующий запуск команды помещает ее в очередь на выполнение.
     */
    const RUN_QUEUE = 2;

    /**
     * Управление запуском команды. Задается константой с префиксом "RUN_".
     *
     * @var int
     */
    protected $behavior = self::RUN_SIMPLE;

    /**
     * Дескриптор файла блокировки.
     *
     * @var resource
     */
    protected $lockHandle;

    /**
     * Возвращает путь к файлу блокировки.
     * По-умолчанию, это "storage/lock/(имя команды).lock".
     *
     * @return string
     */
    protected function getLockPath()
    {
        return storage_path() . '/lock/' . strtolower(str_replace(':', '_', $this->name)) . '.lock';
    }

    /**
     * Возвращает флаги для захвата блокировки.
     *
     * @return int
     */
    protected function getLockFlags()
    {
        switch ($this->behavior) {
            case self::RUN_BLOCKING:
                return LOCK_EX | LOCK_NB;

            case self::RUN_QUEUE:
                return LOCK_EX;

            default:
                return LOCK_UN;
        }
    }

    /**
     * Захват блокировки.
     *
     * @return bool
     */
    protected function lock()
    {
        if (is_resource($this->lockHandle)) {
            $this->free();
        }

        $this->lockHandle = fopen($this->getLockPath(), 'c');

        $lock = false !== $this->lockHandle && flock($this->lockHandle, $this->getLockFlags());

        if (false === $lock) {
            $this->lockHandle = null;
        }

        return $lock;
    }

    /**
     * Освобождение блокировки.
     *
     * @return bool
     */
    protected function free()
    {
        if (!is_resource($this->lockHandle)) {
            return false;
        }

        fflush($this->lockHandle);

        flock($this->lockHandle, LOCK_UN);

        fclose($this->lockHandle);

        return $this->removeLockFile();
    }

    /**
     * Удаление файла блокировки
     *
     * @return bool
     */
    protected function removeLockFile()
    {
        try {
            $file = $this->getLockPath();
            if (is_file($file) && !is_link($file)) {
                if (@unlink($file) === false) {
                    throw new \Exception(sprintf('Failed to remove lock file "%s".', $file));
                }
            } else {
                return false;
            }

            return true;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Проверяет, используется ли блокировка в управлении запуском.
     *
     * @return bool
     */
    protected function isUsedLock()
    {
        return $this->behavior !== self::RUN_SIMPLE;
    }

    /**
     * Execute the console command.
     *
     * @param  \Symfony\Component\Console\Input\InputInterface   $input
     * @param  \Symfony\Component\Console\Output\OutputInterface $output
     * @return mixed
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        if (!$this->isUsedLock() || $this->lock()) {
            $method = method_exists($this, 'handle') ? 'handle' : 'fire';
            call_user_func([$this, $method]);
        }

        $this->free();
    }
}