]> git.mxchange.org Git - friendica.git/commitdiff
Console Lock
authorPhilipp Holzer <admin+github@philipp.info>
Tue, 13 Aug 2019 19:20:41 +0000 (21:20 +0200)
committerPhilipp Holzer <admin+github@philipp.info>
Thu, 15 Aug 2019 12:26:04 +0000 (14:26 +0200)
WIP

src/Console/Lock.php [new file with mode: 0644]
src/Core/Console.php
src/Core/Lock/CacheLock.php
src/Core/Lock/DatabaseLock.php
src/Core/Lock/ILock.php
src/Core/Lock/Lock.php
src/Core/Lock/SemaphoreLock.php
tests/src/Core/Lock/LockTest.php
tests/src/Core/Lock/SemaphoreLockTest.php

diff --git a/src/Console/Lock.php b/src/Console/Lock.php
new file mode 100644 (file)
index 0000000..fe9132b
--- /dev/null
@@ -0,0 +1,185 @@
+<?php
+
+namespace Friendica\Console;
+
+use Asika\SimpleConsole\CommandArgsException;
+use Friendica\App;
+use Friendica\Core\Lock\ILock;
+use RuntimeException;
+
+/**
+ * @brief tool to access the locks from the CLI
+ *
+ * With this script you can access the locks of your node from the CLI.
+ * You can read current locks and set/remove locks.
+ *
+ * @author Philipp Holzer <admin@philipp.info>, Hypolite Petovan <hypolite@mrpetovan.com>
+ */
+class Lock extends \Asika\SimpleConsole\Console
+{
+       protected $helpOptions = ['h', 'help', '?'];
+
+       /**
+        * @var App\Mode
+        */
+       private $appMode;
+
+       /**
+        * @var ILock
+        */
+       private $lock;
+
+       protected function getHelp()
+       {
+               $help = <<<HELP
+console cache - Manage node cache
+Synopsis
+       bin/console lock list [<prefix>] [-h|--help|-?] [-v]
+       bin/console lock set <lock> [<timeout> [<ttl>]] [-h|--help|-?] [-v]
+       bin/console lock del <lock> [-h|--help|-?] [-v]
+       bin/console lock clear [-h|--help|-?] [-v]
+
+Description
+       bin/console lock list [<prefix>]
+               List all locks, optionally filtered by a prefix
+
+       bin/console lock set <lock> [<timeout> [<ttl>]]
+               Sets manually a lock, optionally with the provided TTL (time to live) with a default of five minutes.
+
+       bin/console lock del <lock>
+               Deletes a lock.
+
+       bin/console lock clear
+               Clears all locks
+
+Options
+    -h|--help|-? Show help information
+    -v           Show more debug information.
+HELP;
+               return $help;
+       }
+
+       public function __construct(App\Mode $appMode, ILock $lock, array $argv = null)
+       {
+               parent::__construct($argv);
+
+               $this->appMode = $appMode;
+               $this->lock    = $lock;
+       }
+
+       protected function doExecute()
+       {
+               if ($this->getOption('v')) {
+                       $this->out('Executable: ' . $this->executable);
+                       $this->out('Class: ' . __CLASS__);
+                       $this->out('Arguments: ' . var_export($this->args, true));
+                       $this->out('Options: ' . var_export($this->options, true));
+               }
+
+               if (!$this->appMode->has(App\Mode::DBCONFIGAVAILABLE)) {
+                       $this->out('Database isn\'t ready or populated yet, database cache won\'t be available');
+               }
+
+               if ($this->getOption('v')) {
+                       $this->out('Lock Driver Name: ' . $this->lock->getName());
+                       $this->out('Lock Driver Class: ' . get_class($this->lock));
+               }
+
+               switch ($this->getArgument(0)) {
+                       case 'list':
+                               $this->executeList();
+                               break;
+                       case 'set':
+                               $this->executeSet();
+                               break;
+                       case 'del':
+                               $this->executeDel();
+                               break;
+                       case 'clear':
+                               $this->executeClear();
+                               break;
+               }
+
+               if (count($this->args) == 0) {
+                       $this->out($this->getHelp());
+                       return 0;
+               }
+
+               return 0;
+       }
+
+       private function executeList()
+       {
+               $prefix = $this->getArgument(1, '');
+               $keys   = $this->lock->getLocks($prefix);
+
+               if (empty($prefix)) {
+                       $this->out('Listing all Locks:');
+               } else {
+                       $this->out('Listing all Locks starting with "' . $prefix . '":');
+               }
+
+               $count = 0;
+               foreach ($keys as $key) {
+                       $this->out($key);
+                       $count++;
+               }
+
+               $this->out($count . ' locks found');
+       }
+
+       private function executeDel()
+       {
+               if (count($this->args) >= 2) {
+                       $lock   = $this->getArgument(1);
+
+                       if ($this->lock->releaseLock($lock, true)){
+                               $this->out(sprintf('Lock \'%s\' released.', $lock));
+                       } else {
+                               $this->out(sprintf('Couldn\'t release Lock \'%s\'', $lock));
+                       }
+
+               } else {
+                       throw new CommandArgsException('Too few arguments for del.');
+               }
+       }
+
+       private function executeSet()
+       {
+               if (count($this->args) >= 2) {
+                       $lock      = $this->getArgument(1);
+                       $timeout = intval($this->getArgument(2, false));
+                       $ttl = intval($this->getArgument(3, false));
+
+                       if (is_array($this->lock->isLocked($lock))) {
+                               throw new RuntimeException(sprintf('\'%s\' is already set.', $lock));
+                       }
+
+                       if (!empty($ttl) && !empty($timeout)) {
+                               $result = $this->lock->acquireLock($lock, $timeout, $ttl);
+                       } elseif (!empty($timeout)) {
+                               $result = $this->lock->acquireLock($lock, $timeout);
+                       } else {
+                               $result = $this->lock->acquireLock($lock);
+                       }
+
+                       if ($result) {
+                               $this->out(sprintf('Lock \'%s\' acquired.', $lock));
+                       } else {
+                               $this->out(sprintf('Unable to lock \'%s\'', $lock));
+                       }
+               } else {
+                       throw new CommandArgsException('Too few arguments for set.');
+               }
+       }
+
+       private function executeClear()
+       {
+               $result = $this->lock->releaseAll(true);
+               if ($result) {
+                       $this->out('Locks successfully cleared,');
+               } else {
+                       $this->out('Unable to clear the locks.');
+               }
+       }
+}
index e1654fbef68a08f8fc0828a6e2cd01098708f08a..2ca568c2da906114b0808efbb83294088119c992 100644 (file)
@@ -38,6 +38,7 @@ Commands:
        archivecontact         Archive a contact when you know that it isn't existing anymore
        help                   Show help about a command, e.g (bin/console help config)
        autoinstall            Starts automatic installation of friendica based on values from htconfig.php
+       lock                   Edit site locks
        maintenance            Set maintenance mode for this node
        newpassword            Set a new password for a given user
        php2po                 Generate a messages.po file from a strings.php file
@@ -65,6 +66,7 @@ HELP;
                'globalcommunitysilence' => Friendica\Console\GlobalCommunitySilence::class,
                'archivecontact'         => Friendica\Console\ArchiveContact::class,
                'autoinstall'            => Friendica\Console\AutomaticInstallation::class,
+               'lock'                   => Friendica\Console\Lock::class,
                'maintenance'            => Friendica\Console\Maintenance::class,
                'newpassword'            => Friendica\Console\NewPassword::class,
                'php2po'                 => Friendica\Console\PhpToPo::class,
index 36a7b4edfb2216703eed9cbe50cf3c19bfea9602..238beb705ce93e986773adef7ec898e41a3ab9b6 100644 (file)
@@ -7,6 +7,11 @@ use Friendica\Core\Cache\IMemoryCache;
 
 class CacheLock extends Lock
 {
+       /**
+        * @var string The static prefix of all locks inside the cache
+        */
+       const CACHE_PREFIX = 'lock:';
+
        /**
         * @var \Friendica\Core\Cache\ICache;
         */
@@ -25,7 +30,7 @@ class CacheLock extends Lock
        /**
         * (@inheritdoc)
         */
-       public function acquireLock($key, $timeout = 120, $ttl = Cache::FIVE_MINUTES)
+       public function acquireLock($key, $timeout = 120, $ttl = Cache\Cache::FIVE_MINUTES)
        {
                $got_lock = false;
                $start    = time();
@@ -85,6 +90,46 @@ class CacheLock extends Lock
                return isset($lock) && ($lock !== false);
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       public function getName()
+       {
+               return $this->cache->getName();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getLocks(string $prefix = '')
+       {
+               $locks = $this->cache->getAllKeys(self::CACHE_PREFIX . $prefix);
+
+               array_walk($locks, function (&$lock, $key) {
+                       $lock = substr($lock, strlen(self::CACHE_PREFIX));
+               });
+
+               return $locks;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function releaseAll($override = false)
+       {
+               $success = parent::releaseAll($override);
+
+               $locks = $this->getLocks();
+
+               foreach ($locks as $lock) {
+                       if (!$this->releaseLock($lock, $override)) {
+                               $success = false;
+                       }
+               }
+
+               return $success;
+       }
+
        /**
         * @param string $key The original key
         *
@@ -92,6 +137,6 @@ class CacheLock extends Lock
         */
        private static function getLockKey($key)
        {
-               return "lock:" . $key;
+               return self::CACHE_PREFIX . $key;
        }
 }
index e5274b9b9b53ead7832bf13c56be88aa2bf98e7b..2f409cd3d235fa94f1c6523b9ad02eb50f9455c5 100644 (file)
@@ -92,9 +92,16 @@ class DatabaseLock extends Lock
        /**
         * (@inheritdoc)
         */
-       public function releaseAll()
+       public function releaseAll($override = false)
        {
-               $return = $this->dba->delete('locks', ['pid' => $this->pid]);
+               $success = parent::releaseAll($override);
+
+               if ($override) {
+                       $where = ['1 = 1'];
+               } else {
+                       $where = ['pid' => $this->pid];
+               }
+               $return = $this->dba->delete('locks', $where);
 
                $this->acquiredLocks = [];
 
@@ -114,4 +121,34 @@ class DatabaseLock extends Lock
                        return false;
                }
        }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getName()
+       {
+               return self::TYPE_DATABASE;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getLocks(string $prefix = '')
+       {
+               if (empty($prefix)) {
+                       $where = ['`expires` >= ?', DateTimeFormat::utcNow()];
+               } else {
+                       $where = ['`expires` >= ? AND `k` LIKE CONCAT(?, \'%\')', DateTimeFormat::utcNow(), $prefix];
+               }
+
+               $stmt = $this->dba->select('locks', ['name'], $where);
+
+               $keys = [];
+               while ($key = $this->dba->fetch($stmt)) {
+                       array_push($keys, $key['name']);
+               }
+               $this->dba->close($stmt);
+
+               return $keys;
+       }
 }
index 0b91daeb568d9bc808d3d907cef4a29e504e81af..d103d991912b42210c0ae4cd3a9766d788eae222 100644 (file)
@@ -45,7 +45,25 @@ interface ILock
        /**
         * Releases all lock that were set by us
         *
+        * @param bool $override Override to release all locks
+        *
         * @return boolean Was the unlock of all locks successful?
         */
-       public function releaseAll();
+       public function releaseAll($override = false);
+
+       /**
+        * Returns the name of the current lock
+        *
+        * @return string
+        */
+       public function getName();
+
+       /**
+        * Lists all locks
+        *
+        * @param string prefix optional a prefix to search
+        *
+        * @return array Empty if it isn't supported by the cache driver
+        */
+       public function getLocks(string $prefix = '');
 }
index 4418fee271bf029e1549cbabfa7fc05600dcd85d..f03ffe03d144e565f7f99f581b6685f46650edb2 100644 (file)
@@ -2,6 +2,8 @@
 
 namespace Friendica\Core\Lock;
 
+use Friendica\Core\Cache\Cache;
+
 /**
  * Class AbstractLock
  *
@@ -11,6 +13,9 @@ namespace Friendica\Core\Lock;
  */
 abstract class Lock implements ILock
 {
+       const TYPE_DATABASE  = Cache::TYPE_DATABASE;
+       const TYPE_SEMAPHORE = 'semaphore';
+
        /**
         * @var array The local acquired locks
         */
@@ -49,16 +54,14 @@ abstract class Lock implements ILock
        }
 
        /**
-        * Releases all lock that were set by us
-        *
-        * @return boolean Was the unlock of all locks successful?
+        * {@inheritDoc}
         */
-       public function releaseAll()
+       public function releaseAll($override = false)
        {
                $return = true;
 
                foreach ($this->acquiredLocks as $acquiredLock => $hasLock) {
-                       if (!$this->releaseLock($acquiredLock)) {
+                       if (!$this->releaseLock($acquiredLock, $override)) {
                                $return = false;
                        }
                }
index 789c9e8ecac2527c853d6cb1b131dcb4d73757be..75c7284a5fd8085c64aba0c485def8bdad92d9da 100644 (file)
@@ -20,9 +20,7 @@ class SemaphoreLock extends Lock
         */
        private static function semaphoreKey($key)
        {
-               $temp = get_temppath();
-
-               $file = $temp . '/' . $key . '.sem';
+               $file = self::keyToFile($key);
 
                if (!file_exists($file)) {
                        file_put_contents($file, $key);
@@ -31,10 +29,24 @@ class SemaphoreLock extends Lock
                return ftok($file, 'f');
        }
 
+       /**
+        * Returns the full path to the semaphore file
+        *
+        * @param string $key The key of the semaphore
+        *
+        * @return string The full path
+        */
+       private static function keyToFile($key)
+       {
+               $temp = get_temppath();
+
+               return $temp . '/' . $key . '.sem';
+       }
+
        /**
         * (@inheritdoc)
         */
-       public function acquireLock($key, $timeout = 120, $ttl = Cache::FIVE_MINUTES)
+       public function acquireLock($key, $timeout = 120, $ttl = Cache\Cache::FIVE_MINUTES)
        {
                self::$semaphore[$key] = sem_get(self::semaphoreKey($key));
                if (self::$semaphore[$key]) {
@@ -52,14 +64,24 @@ class SemaphoreLock extends Lock
         */
        public function releaseLock($key, $override = false)
        {
-               if (empty(self::$semaphore[$key])) {
-                       return false;
-               } else {
-                       $success = @sem_release(self::$semaphore[$key]);
-                       unset(self::$semaphore[$key]);
-                       $this->markRelease($key);
-                       return $success;
+               $success = false;
+
+               if (!empty(self::$semaphore[$key])) {
+                       try {
+                               $success = @sem_release(self::$semaphore[$key]) &&
+                                          unlink(self::keyToFile($key));
+                               unset(self::$semaphore[$key]);
+                               $this->markRelease($key);
+                       } catch (\Exception $exception) {
+                               $success = false;
+                       }
+               } else if ($override) {
+                       if ($this->acquireLock($key)) {
+                               $success = $this->releaseLock($key, true);
+                       }
                }
+
+               return $success;
        }
 
        /**
@@ -69,4 +91,47 @@ class SemaphoreLock extends Lock
        {
                return isset(self::$semaphore[$key]);
        }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getName()
+       {
+               return self::TYPE_SEMAPHORE;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function getLocks(string $prefix = '')
+       {
+               $temp = get_temppath();
+               $locks = [];
+               foreach (glob(sprintf('%s/%s*.sem', $temp, $prefix)) as $lock) {
+                       $lock = pathinfo($lock, PATHINFO_FILENAME);
+                       if(sem_get(self::semaphoreKey($lock))) {
+                               $locks[] = $lock;
+                       }
+               }
+
+               return $locks;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       public function releaseAll($override = false)
+       {
+               $success = parent::releaseAll($override);
+
+               $temp = get_temppath();
+               foreach (glob(sprintf('%s/*.sem', $temp)) as $lock) {
+                       $lock = pathinfo($lock, PATHINFO_FILENAME);
+                       if (!$this->releaseLock($lock, true)) {
+                               $success = false;
+                       }
+               }
+
+               return $success;
+       }
 }
index 0c231713ae670c1032a1802a8e3f8f730374426f..dd38172b38d0b0c7e4d225dd119cfd819998fc50 100644 (file)
@@ -23,12 +23,12 @@ abstract class LockTest extends MockedTest
                parent::setUp();
 
                $this->instance = $this->getInstance();
-               $this->instance->releaseAll();
+               $this->instance->releaseAll(true);
        }
 
        protected function tearDown()
        {
-               $this->instance->releaseAll();
+               $this->instance->releaseAll(true);
                parent::tearDown();
        }
 
@@ -123,6 +123,46 @@ abstract class LockTest extends MockedTest
                $this->assertFalse($this->instance->isLocked('test'));
        }
 
+       /**
+        * @small
+        */
+       public function testGetLocks()
+       {
+               $this->assertTrue($this->instance->acquireLock('foo', 1));
+               $this->assertTrue($this->instance->acquireLock('bar', 1));
+               $this->assertTrue($this->instance->acquireLock('nice', 1));
+
+               $this->assertTrue($this->instance->isLocked('foo'));
+               $this->assertTrue($this->instance->isLocked('bar'));
+               $this->assertTrue($this->instance->isLocked('nice'));
+
+               $locks = $this->instance->getLocks();
+
+               $this->assertContains('foo', $locks);
+               $this->assertContains('bar', $locks);
+               $this->assertContains('nice', $locks);
+       }
+
+       /**
+        * @small
+        */
+       public function testGetLocksWithPrefix()
+       {
+               $this->assertTrue($this->instance->acquireLock('foo', 1));
+               $this->assertTrue($this->instance->acquireLock('test1', 1));
+               $this->assertTrue($this->instance->acquireLock('test2', 1));
+
+               $this->assertTrue($this->instance->isLocked('foo'));
+               $this->assertTrue($this->instance->isLocked('test1'));
+               $this->assertTrue($this->instance->isLocked('test2'));
+
+               $locks = $this->instance->getLocks('test');
+
+               $this->assertContains('test1', $locks);
+               $this->assertContains('test2', $locks);
+               $this->assertNotContains('foo', $locks);
+       }
+
        /**
         * @medium
         */
index 7b9b03d728103a0c86c1ea4b59a0b769b436d20e..52c5aaa5b88cb7b48653de9b9b20538cde588c64 100644 (file)
@@ -12,8 +12,6 @@ class SemaphoreLockTest extends LockTest
 {
        public function setUp()
        {
-               parent::setUp();
-
                $dice = \Mockery::mock(Dice::class)->makePartial();
 
                $app = \Mockery::mock(App::class);
@@ -29,6 +27,8 @@ class SemaphoreLockTest extends LockTest
 
                // @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject
                BaseObject::setDependencyInjection($dice);
+
+               parent::setUp();
        }
 
        protected function getInstance()