Spaces:
No application file
No application file
File size: 7,216 Bytes
d2897cd |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
<?php
namespace Mautic\CoreBundle\Command;
use Mautic\CoreBundle\Helper\CoreParametersHelper;
use Mautic\CoreBundle\Helper\PathsHelper;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\Store\FlockStore;
use Symfony\Component\Lock\Store\RedisStore;
abstract class ModeratedCommand extends Command
{
public const MODE_PID = 'pid';
public const MODE_FLOCK = 'flock';
public const MODE_REDIS = 'redis';
/**
* @deprecated Symfony 4 Removed LockHandler and the replacement is the lock from the Lock component so there is no need for something custom
*/
public const MODE_LOCK = 'file_lock';
protected $checkFile;
protected $moderationKey;
protected $moderationTable = [];
protected $moderationMode;
protected $runDirectory;
protected $lockExpiration;
protected $lockFile;
private ?\Symfony\Component\Lock\LockInterface $lock = null;
/**
* @var OutputInterface
*/
protected $output;
public function __construct(
protected PathsHelper $pathsHelper,
private CoreParametersHelper $coreParametersHelper
) {
parent::__construct();
}
/**
* Set moderation options.
*/
protected function configure()
{
$this
->addOption('--bypass-locking', null, InputOption::VALUE_NONE, 'Bypass locking.')
->addOption(
'--timeout',
'-t',
InputOption::VALUE_REQUIRED,
'If getmypid() is disabled on this system, lock files will be used. This option will assume the process is dead after the specified number of seconds and will execute anyway. This is disabled by default.',
null
)
->addOption(
'--lock_mode',
'-x',
InputOption::VALUE_REQUIRED,
'Allowed value are "pid", "flock" or redis. By default, lock will try with pid, if not available will use file system',
self::MODE_PID
)
->addOption('--force', '-f', InputOption::VALUE_NONE, 'Deprecated; use --bypass-locking instead.');
}
protected function checkRunStatus(InputInterface $input, OutputInterface $output, $moderationKey = ''): bool
{
// Bypass locking
if ((bool) $input->getOption('bypass-locking') || (bool) $input->getOption('force')) {
return true;
}
$this->output = $output;
$this->lockExpiration = $input->getOption('timeout');
if (null !== $this->lockExpiration) {
$this->lockExpiration = (float) $this->lockExpiration;
}
$this->moderationMode = $input->getOption('lock_mode');
if (self::MODE_LOCK === $this->moderationMode) {
// File lock is deprecated in favor of Symfony's Lock component's lock
$this->moderationMode = 'flock';
}
if (!in_array($this->moderationMode, [self::MODE_PID, self::MODE_FLOCK, self::MODE_REDIS])) {
$output->writeln('<error>Unknown locking method specified.</error>');
return false;
}
// Allow multiple runs of the same command if executing different IDs, etc
$this->moderationKey = $this->getName().$moderationKey;
if (in_array($this->moderationMode, [self::MODE_PID, self::MODE_FLOCK])) {
// Setup the run directory for lock/pid files
$this->runDirectory = $this->pathsHelper->getSystemPath('cache').'/../run';
if (!file_exists($this->runDirectory) && !@mkdir($this->runDirectory)) {
// This needs to throw an exception in order to not silently fail when there is an issue
throw new \RuntimeException($this->runDirectory.' could not be created.');
}
}
// Check if the command is currently running
if (!$this->checkStatus()) {
$output->writeln('<error>Script in progress. Can force execution by using --bypass-locking.</error>');
return false;
}
return true;
}
/**
* Complete this run.
*/
protected function completeRun(): void
{
if ($this->lock) {
$this->lock->release();
}
// Attempt to keep things tidy
@unlink($this->lockFile);
}
private function checkStatus(): bool
{
if (self::MODE_PID === $this->moderationMode && $this->isPidSupported()) {
return $this->checkPid();
}
return $this->checkFlock();
}
private function checkPid(): bool
{
$this->lockFile = sprintf(
'%s/sf.%s.%s.lock',
$this->runDirectory,
preg_replace('/[^a-z0-9\._-]+/i', '-', $this->moderationKey),
hash('sha256', $this->moderationKey)
);
// Check if the PID is still running
$fp = fopen($this->lockFile, 'c+');
if (!flock($fp, LOCK_EX)) {
$this->output->writeln("<error>Failed to lock {$this->lockFile}.</error>");
return false;
}
$pid = fgets($fp, 8192);
if ($pid && posix_getpgid((int) $pid)) {
$this->output->writeln('<info>Script with pid '.$pid.' in progress.</info>');
flock($fp, LOCK_UN);
fclose($fp);
return false;
}
// Write current PID to lock file
ftruncate($fp, 0);
rewind($fp);
fputs($fp, (string) getmypid());
fflush($fp);
flock($fp, LOCK_UN);
fclose($fp);
return true;
}
private function checkFlock(): bool
{
switch ($this->moderationMode) {
case self::MODE_REDIS:
$cacheAdapterConfig = $this->coreParametersHelper->get('cache_adapter_redis');
$redisDsn = $cacheAdapterConfig['dsn'] ?? null;
$redisOptions = $cacheAdapterConfig['options'] ?? [];
$redis = RedisAdapter::createConnection($redisDsn, $redisOptions);
$store = new RedisStore($redis, $this->lockExpiration ?? 3600);
break;
default:
$store = new FlockStore($this->runDirectory);
}
$factory = new LockFactory($store);
$this->lock = $factory->createLock($this->moderationKey, $this->lockExpiration);
return $this->lock->acquire();
}
public function isPidSupported(): bool
{
// getmypid may be disabled and posix_getpgid is not available on Windows machines
if (!function_exists('getmypid') || !function_exists('posix_getpgid')) {
return false;
}
$disabled = explode(',', ini_get('disable_functions'));
if (in_array('getmypid', $disabled) || in_array('posix_getpgid', $disabled)) {
return false;
}
return true;
}
}
|