--- /dev/null
+<?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.');
+ }
+ }
+}
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
'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,
class CacheLock extends Lock
{
+ /**
+ * @var string The static prefix of all locks inside the cache
+ */
+ const CACHE_PREFIX = 'lock:';
+
/**
* @var \Friendica\Core\Cache\ICache;
*/
/**
* (@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();
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
*
*/
private static function getLockKey($key)
{
- return "lock:" . $key;
+ return self::CACHE_PREFIX . $key;
}
}
/**
* (@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 = [];
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;
+ }
}
/**
* 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 = '');
}
namespace Friendica\Core\Lock;
+use Friendica\Core\Cache\Cache;
+
/**
* Class AbstractLock
*
*/
abstract class Lock implements ILock
{
+ const TYPE_DATABASE = Cache::TYPE_DATABASE;
+ const TYPE_SEMAPHORE = 'semaphore';
+
/**
* @var array The local acquired locks
*/
}
/**
- * 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;
}
}
*/
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);
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]) {
*/
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;
}
/**
{
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;
+ }
}
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();
}
$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
*/
{
public function setUp()
{
- parent::setUp();
-
$dice = \Mockery::mock(Dice::class)->makePartial();
$app = \Mockery::mock(App::class);
// @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject
BaseObject::setDependencyInjection($dice);
+
+ parent::setUp();
}
protected function getInstance()