1 | <?php |
||||
2 | |||||
3 | /* |
||||
4 | * @copyright 2015 Mautic Contributors. All rights reserved |
||||
5 | * @author Mautic |
||||
6 | * |
||||
7 | * @link http://mautic.org |
||||
8 | * |
||||
9 | * @license GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html |
||||
10 | */ |
||||
11 | |||||
12 | namespace Mautic\CoreBundle\Command; |
||||
13 | |||||
14 | use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; |
||||
15 | use Symfony\Component\Console\Input\InputInterface; |
||||
16 | use Symfony\Component\Console\Input\InputOption; |
||||
17 | use Symfony\Component\Console\Output\OutputInterface; |
||||
18 | use Symfony\Component\Filesystem\Exception\IOException; |
||||
19 | use Symfony\Component\Filesystem\LockHandler; |
||||
20 | |||||
21 | abstract class ModeratedCommand extends ContainerAwareCommand |
||||
22 | { |
||||
23 | const MODE_LOCK = 'lock'; |
||||
24 | const MODE_PID = 'pid'; |
||||
25 | const MODE_FLOCK = 'flock'; |
||||
26 | |||||
27 | protected $checkFile; |
||||
28 | protected $moderationKey; |
||||
29 | protected $moderationTable = []; |
||||
30 | protected $moderationMode = self::MODE_LOCK; |
||||
31 | protected $runDirectory; |
||||
32 | protected $lockExpiration = false; |
||||
33 | protected $lockHandler; |
||||
34 | protected $lockFile; |
||||
35 | private $bypassLocking; |
||||
36 | |||||
37 | private $flockHandle; |
||||
38 | |||||
39 | /* @var OutputInterface $output */ |
||||
40 | protected $output; |
||||
41 | |||||
42 | /** |
||||
43 | * Set moderation options. |
||||
44 | */ |
||||
45 | protected function configure() |
||||
46 | { |
||||
47 | $this |
||||
48 | ->addOption('--force', '-f', InputOption::VALUE_NONE, 'Force execution even if another process is assumed running.') |
||||
49 | ->addOption('--bypass-locking', null, InputOption::VALUE_NONE, 'Bypass locking.') |
||||
50 | ->addOption( |
||||
51 | '--timeout', |
||||
52 | '-t', |
||||
53 | InputOption::VALUE_REQUIRED, |
||||
54 | '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.', |
||||
55 | false |
||||
56 | ) |
||||
57 | ->addOption( |
||||
58 | '--lock_mode', |
||||
59 | '-x', |
||||
60 | InputOption::VALUE_REQUIRED, |
||||
61 | 'Allowed value are "pid" , "file_lock" or "flock". By default, lock will try with pid, if not available will use file system', |
||||
62 | 'pid' |
||||
63 | ); |
||||
64 | } |
||||
65 | |||||
66 | /** |
||||
67 | * @return bool |
||||
68 | */ |
||||
69 | protected function checkRunStatus(InputInterface $input, OutputInterface $output, $moderationKey = '') |
||||
70 | { |
||||
71 | $this->output = $output; |
||||
72 | $this->lockExpiration = $input->getOption('timeout'); |
||||
73 | $this->bypassLocking = $input->getOption('bypass-locking'); |
||||
74 | $lockMode = $input->getOption('lock_mode'); |
||||
75 | |||||
76 | if (!in_array($lockMode, ['pid', 'file_lock', 'flock'])) { |
||||
77 | $output->writeln('<error>Unknown locking method specified.</error>'); |
||||
78 | |||||
79 | return false; |
||||
80 | } |
||||
81 | |||||
82 | // If bypass locking, then don't bother locking |
||||
83 | if ($this->bypassLocking) { |
||||
84 | return true; |
||||
85 | } |
||||
86 | |||||
87 | // Allow multiple runs of the same command if executing different IDs, etc |
||||
88 | $this->moderationKey = $this->getName().$moderationKey; |
||||
89 | |||||
90 | // Setup the run directory for lock/pid files |
||||
91 | $this->runDirectory = $this->getContainer()->getParameter('kernel.cache_dir').'/../run'; |
||||
92 | if (!file_exists($this->runDirectory)) { |
||||
93 | if (!mkdir($this->runDirectory)) { |
||||
94 | $output->writeln('<error>'.$this->runDirectory.' could not be created.</error>'); |
||||
95 | |||||
96 | return false; |
||||
97 | } |
||||
98 | } |
||||
99 | |||||
100 | $this->lockFile = sprintf( |
||||
101 | '%s/sf.%s.%s.lock', |
||||
102 | $this->runDirectory, |
||||
103 | preg_replace('/[^a-z0-9\._-]+/i', '-', $this->moderationKey), |
||||
104 | hash('sha256', $this->moderationKey) |
||||
105 | ); |
||||
106 | |||||
107 | // Check if the command is currently running |
||||
108 | if (!$this->checkStatus($input->getOption('force'), $lockMode)) { |
||||
109 | $output->writeln('<error>Script in progress. Can force execution by using --force.</error>'); |
||||
110 | |||||
111 | return false; |
||||
112 | } |
||||
113 | |||||
114 | return true; |
||||
115 | } |
||||
116 | |||||
117 | /** |
||||
118 | * Complete this run. |
||||
119 | */ |
||||
120 | protected function completeRun() |
||||
121 | { |
||||
122 | if ($this->bypassLocking) { |
||||
123 | return; |
||||
124 | } |
||||
125 | |||||
126 | if (self::MODE_LOCK == $this->moderationMode) { |
||||
127 | $this->lockHandler->release(); |
||||
128 | } |
||||
129 | if (self::MODE_FLOCK == $this->moderationMode) { |
||||
130 | fclose($this->flockHandle); |
||||
131 | } |
||||
132 | |||||
133 | // Attempt to keep things tidy |
||||
134 | @unlink($this->lockFile); |
||||
0 ignored issues
–
show
|
|||||
135 | } |
||||
136 | |||||
137 | /** |
||||
138 | * Determine the moderation mode avaiable to this system. Default is to use a lock file. |
||||
139 | * |
||||
140 | * @param bool $force |
||||
141 | * @param string $lockMode |
||||
142 | * |
||||
143 | * @return bool |
||||
144 | */ |
||||
145 | private function checkStatus($force = false, $lockMode = null) |
||||
146 | { |
||||
147 | // getmypid may be disabled and posix_getpgid is not available on Windows machines |
||||
148 | if ((is_null($lockMode) || 'pid' === $lockMode) && function_exists('getmypid') && function_exists('posix_getpgid')) { |
||||
149 | $disabled = explode(',', ini_get('disable_functions')); |
||||
150 | if (!in_array('getmypid', $disabled) && !in_array('posix_getpgid', $disabled)) { |
||||
151 | $this->moderationMode = self::MODE_PID; |
||||
152 | |||||
153 | // Check if the PID is still running |
||||
154 | $fp = fopen($this->lockFile, 'c+'); |
||||
155 | if (!flock($fp, LOCK_EX)) { |
||||
156 | $this->output->writeln("<error>Failed to lock {$this->lockFile}.</error>"); |
||||
157 | |||||
158 | return false; |
||||
159 | } |
||||
160 | |||||
161 | $pid = fgets($fp, 8192); |
||||
162 | if (!$force && $pid && posix_getpgid($pid)) { |
||||
163 | $this->output->writeln('<info>Script with pid '.$pid.' in progress.</info>'); |
||||
164 | |||||
165 | flock($fp, LOCK_UN); |
||||
166 | fclose($fp); |
||||
167 | |||||
168 | return false; |
||||
169 | } |
||||
170 | |||||
171 | // Write current PID to lock file |
||||
172 | ftruncate($fp, 0); |
||||
173 | rewind($fp); |
||||
174 | |||||
175 | fputs($fp, getmypid()); |
||||
176 | fflush($fp); |
||||
177 | |||||
178 | flock($fp, LOCK_UN); |
||||
179 | fclose($fp); |
||||
180 | |||||
181 | return true; |
||||
182 | } |
||||
183 | } elseif (self::MODE_FLOCK === $lockMode && !$force) { |
||||
184 | $this->moderationMode = self::MODE_FLOCK; |
||||
185 | $error = null; |
||||
186 | // Silence error reporting |
||||
187 | set_error_handler(function ($errno, $msg) use (&$error) { |
||||
188 | $error = $msg; |
||||
189 | }); |
||||
190 | |||||
191 | if (!$this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r')) { |
||||
192 | if ($this->flockHandle = fopen($this->lockFile, 'x')) { |
||||
193 | chmod($this->lockFile, 0666); |
||||
194 | } elseif (!$this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r')) { |
||||
195 | usleep(100); |
||||
196 | $this->flockHandle = fopen($this->lockFile, 'r+') ?: fopen($this->lockFile, 'r'); |
||||
197 | } |
||||
198 | } |
||||
199 | |||||
200 | restore_error_handler(); |
||||
201 | |||||
202 | if (!$this->flockHandle) { |
||||
203 | throw new IOException($error, 0, null, $this->lockFile); |
||||
204 | } |
||||
205 | if (!flock($this->flockHandle, LOCK_EX | LOCK_NB)) { |
||||
206 | fclose($this->flockHandle); |
||||
207 | $this->flockHandle = null; |
||||
208 | |||||
209 | return false; |
||||
210 | } |
||||
211 | |||||
212 | return true; |
||||
213 | } |
||||
214 | |||||
215 | // in anycase, fallback on file system |
||||
216 | // Accessing PID commands is not available so use a simple lock file mechanism |
||||
217 | $lockHandler = $this->lockHandler = new LockHandler($this->moderationKey, $this->runDirectory); |
||||
218 | |||||
219 | if (!$force && !$lockHandler->lock()) { |
||||
220 | // Check timestamp if $force is not requested |
||||
221 | if ($this->lockExpiration) { |
||||
222 | $fileAge = time() - filemtime($this->lockFile); |
||||
223 | |||||
224 | if ($fileAge <= $this->lockExpiration) { |
||||
225 | $this->output->writeln('<info>Lock expires in '.($this->lockExpiration - $fileAge).' seconds.</info>'); |
||||
226 | |||||
227 | return false; |
||||
228 | } |
||||
229 | } else { |
||||
230 | // Lock is still in effect |
||||
231 | return false; |
||||
232 | } |
||||
233 | } elseif (!$force) { |
||||
234 | // Attempt to update the modified time just in case there was no lock but the file still exists |
||||
235 | @touch($this->lockFile); |
||||
0 ignored issues
–
show
It seems like you do not handle an error condition for
touch() . This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
![]() |
|||||
236 | } |
||||
237 | |||||
238 | return true; |
||||
239 | } |
||||
240 | } |
||||
241 |
If you suppress an error, we recommend checking for the error condition explicitly: