397f2466908334aacef38a23386258d1a5bfb36d
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Locking / Locker.php
1 <?php
2 namespace TYPO3\CMS\Core\Locking;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18
19 /**
20 * TYPO3 locking class
21 * This class provides an abstract layer to various locking features for TYPO3
22 *
23 * It is intended to blocks requests until some data has been generated.
24 * This is especially useful if two clients are requesting the same website short after each other. While the request of client 1 triggers building and caching of the website, client 2 will be waiting at this lock.
25 *
26 * @author Michael Stucki <michael@typo3.org>
27 * @author Markus Klein <klein.t3@mfc-linz.at>
28 */
29 class Locker {
30
31 const LOCKING_METHOD_SIMPLE = 'simple';
32 const LOCKING_METHOD_FLOCK = 'flock';
33 const LOCKING_METHOD_SEMAPHORE = 'semaphore';
34 const LOCKING_METHOD_DISABLED = 'disable';
35
36 const FILE_LOCK_FOLDER = 'typo3temp/locks/';
37
38 /**
39 * @var string Locking method: One of the constants above
40 */
41 protected $method = '';
42
43 /**
44 * @var mixed Identifier used for this lock
45 */
46 protected $id;
47
48 /**
49 * @var mixed Resource used for this lock (can be a file or a semaphore resource)
50 */
51 protected $resource;
52
53 /**
54 * @var resource File pointer if using flock method
55 */
56 protected $filePointer;
57
58 /**
59 * @var bool True if lock is acquired
60 */
61 protected $isAcquired = FALSE;
62
63 /**
64 * @var int Number of times a locked resource is tried to be acquired. Only used in manual locks method "simple".
65 */
66 protected $loops = 150;
67
68 /**
69 * @var int Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock. Only used in manual lock method "simple".
70 */
71 protected $step = 200;
72
73 /**
74 * @var string Logging facility
75 */
76 protected $syslogFacility = 'cms';
77
78 /**
79 * @var bool True if locking should be logged
80 */
81 protected $isLoggingEnabled = TRUE;
82
83 /**
84 * Constructor:
85 * initializes locking, check input parameters and set variables accordingly.
86 *
87 * Parameters $loops and $step only apply to the locking method LOCKING_METHOD_SIMPLE.
88 *
89 * @param string $id ID to identify this lock in the system
90 * @param string $method Define which locking method to use. Use one of the LOCKING_METHOD_* constants. Defaults to LOCKING_METHOD_SIMPLE. Use '' to use setting from Install Tool.
91 * @param int $loops Number of times a locked resource is tried to be acquired.
92 * @param int $step Milliseconds after lock acquire is retried. $loops * $step results in the maximum delay of a lock.
93 * @throws \RuntimeException
94 * @throws \InvalidArgumentException
95 */
96 public function __construct($id, $method = self::LOCKING_METHOD_SIMPLE, $loops = 0, $step = 0) {
97 // Force ID to be string
98 $id = (string)$id;
99 if ((int)$loops) {
100 $this->loops = (int)$loops;
101 }
102 if ((int)$step) {
103 $this->step = (int)$step;
104 }
105 if ($method === '' && isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'])) {
106 $method = (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'];
107 }
108
109 switch ($method) {
110 case self::LOCKING_METHOD_SIMPLE:
111 // intended fall through
112 case self::LOCKING_METHOD_FLOCK:
113 $this->id = md5($id);
114 $this->createPathIfNeeded();
115 break;
116 case self::LOCKING_METHOD_SEMAPHORE:
117 $this->id = abs(crc32($id));
118 break;
119 case self::LOCKING_METHOD_DISABLED:
120 break;
121 default:
122 throw new \InvalidArgumentException('No such locking method "' . $method . '"', 1294586097);
123 }
124 $this->method = $method;
125 }
126
127 /**
128 * Destructor:
129 * Releases lock automatically when instance is destroyed and release resources
130 */
131 public function __destruct() {
132 $this->release();
133 switch ($this->method) {
134 case self::LOCKING_METHOD_FLOCK:
135 if (
136 GeneralUtility::isAllowedAbsPath($this->resource)
137 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
138 ) {
139 @unlink($this->resource);
140 }
141 break;
142 case self::LOCKING_METHOD_SEMAPHORE:
143 @sem_remove($this->resource);
144 break;
145 default:
146 // do nothing
147 }
148 }
149
150 /**
151 * Tries to allocate the semaphore
152 *
153 * @return void
154 * @throws \RuntimeException
155 */
156 protected function getSemaphore() {
157 $this->resource = sem_get($this->id, 1);
158 if ($this->resource === FALSE) {
159 throw new \RuntimeException('Unable to get semaphore with id ' . $this->id, 1313828196);
160 }
161 }
162
163 /**
164 * Acquire a lock and return when successful.
165 *
166 * It is important to know that the lock will be acquired in any case, even if the request was blocked first.
167 * Therefore, the lock needs to be released in every situation.
168 *
169 * @return bool Returns TRUE if lock could be acquired without waiting, FALSE otherwise.
170 * @throws \RuntimeException
171 * @deprecated since 6.2 - will be removed two versions later; use new API instead
172 */
173 public function acquire() {
174 // TODO refactor locking in TSFE to use the new API, then this call can be logged
175 // GeneralUtility::logDeprecatedFunction();
176
177 // Default is TRUE, which means continue without caring for other clients.
178 // In the case of TYPO3s cache management, this has no negative effect except some resource overhead.
179 $noWait = FALSE;
180 $isAcquired = FALSE;
181 switch ($this->method) {
182 case self::LOCKING_METHOD_SIMPLE:
183 if (file_exists($this->resource)) {
184 $this->sysLog('Waiting for a different process to release the lock');
185 $maxExecutionTime = (int)ini_get('max_execution_time');
186 $maxAge = time() - ($maxExecutionTime ?: 120);
187 if (@filectime($this->resource) < $maxAge) {
188 @unlink($this->resource);
189 $this->sysLog('Unlinking stale lockfile');
190 }
191 }
192 for ($i = 0; $i < $this->loops; $i++) {
193 $filePointer = @fopen($this->resource, 'x');
194 if ($filePointer !== FALSE) {
195 fclose($filePointer);
196 GeneralUtility::fixPermissions($this->resource);
197 $this->sysLog('Lock acquired');
198 $noWait = $i === 0;
199 $isAcquired = TRUE;
200 break;
201 }
202 usleep($this->step * 1000);
203 }
204 if (!$isAcquired) {
205 throw new \RuntimeException('Lock file could not be created', 1294586098);
206 }
207 break;
208 case self::LOCKING_METHOD_FLOCK:
209 $this->filePointer = fopen($this->resource, 'c');
210 if ($this->filePointer === FALSE) {
211 throw new \RuntimeException('Lock file could not be opened', 1294586099);
212 }
213 // Lock without blocking
214 if (flock($this->filePointer, LOCK_EX | LOCK_NB)) {
215 $noWait = TRUE;
216 } elseif (flock($this->filePointer, LOCK_EX)) {
217 // Lock with blocking (waiting for similar locks to become released)
218 $noWait = FALSE;
219 } else {
220 throw new \RuntimeException('Could not lock file "' . $this->resource . '"', 1294586100);
221 }
222 $isAcquired = TRUE;
223 break;
224 case self::LOCKING_METHOD_SEMAPHORE:
225 $this->getSemaphore();
226 while (!$isAcquired) {
227 if (@sem_acquire($this->resource)) {
228 // Unfortunately it is not possible to find out if the request has blocked,
229 // as sem_acquire will block until we get the resource.
230 // So we do not set $noWait here at all
231 $isAcquired = TRUE;
232 }
233 }
234 break;
235 case self::LOCKING_METHOD_DISABLED:
236 break;
237 default:
238 // will never be reached
239 }
240 $this->isAcquired = $isAcquired;
241 return $noWait;
242 }
243
244 /**
245 * Try to acquire an exclusive lock
246 *
247 * @throws \RuntimeException
248 * @return bool Returns TRUE if the lock was acquired successfully
249 */
250 public function acquireExclusiveLock() {
251 if ($this->isAcquired) {
252 return TRUE;
253 }
254 $this->isAcquired = FALSE;
255 switch ($this->method) {
256 case self::LOCKING_METHOD_SIMPLE:
257 if (file_exists($this->resource)) {
258 $this->sysLog('Waiting for a different process to release the lock');
259 $maxExecutionTime = (int)ini_get('max_execution_time');
260 $maxAge = time() - ($maxExecutionTime ?: 120);
261 if (@filectime($this->resource) < $maxAge) {
262 @unlink($this->resource);
263 $this->sysLog('Unlinking stale lockfile');
264 }
265 }
266 for ($i = 0; $i < $this->loops; $i++) {
267 $filePointer = @fopen($this->resource, 'x');
268 if ($filePointer !== FALSE) {
269 fclose($filePointer);
270 GeneralUtility::fixPermissions($this->resource);
271 $this->sysLog('Lock acquired');
272 $this->isAcquired = TRUE;
273 break;
274 }
275 usleep($this->step * 1000);
276 }
277 break;
278 case self::LOCKING_METHOD_FLOCK:
279 $this->filePointer = fopen($this->resource, 'c');
280 if ($this->filePointer === FALSE) {
281 throw new \RuntimeException('Lock file could not be opened', 1294586099);
282 }
283 if (flock($this->filePointer, LOCK_EX)) {
284 $this->isAcquired = TRUE;
285 }
286 break;
287 case self::LOCKING_METHOD_SEMAPHORE:
288 $this->getSemaphore();
289 if (@sem_acquire($this->resource)) {
290 $this->isAcquired = TRUE;
291 }
292 break;
293 case self::LOCKING_METHOD_DISABLED:
294 break;
295 default:
296 // will never be reached
297 }
298 return $this->isAcquired;
299 }
300
301 /**
302 * Try to acquire a shared lock
303 *
304 * (Only works for the flock() locking method currently)
305 *
306 * @return bool Returns TRUE if the lock was acquired successfully
307 * @throws \RuntimeException
308 */
309 public function acquireSharedLock() {
310 if ($this->isAcquired) {
311 return TRUE;
312 }
313 if ($this->method === self::LOCKING_METHOD_FLOCK) {
314 $this->filePointer = fopen($this->resource, 'c');
315 if ($this->filePointer === FALSE) {
316 throw new \RuntimeException('Lock file could not be opened', 1294586099);
317 }
318 if (flock($this->filePointer, LOCK_SH)) {
319 $this->isAcquired = TRUE;
320 }
321 }
322 return $this->isAcquired;
323 }
324
325 /**
326 * Release the lock
327 *
328 * @return bool Returns TRUE on success or FALSE on failure
329 */
330 public function release() {
331 if (!$this->isAcquired) {
332 return TRUE;
333 }
334 $success = TRUE;
335 switch ($this->method) {
336 case self::LOCKING_METHOD_SIMPLE:
337 if (
338 GeneralUtility::isAllowedAbsPath($this->resource)
339 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
340 ) {
341 if (@unlink($this->resource) === FALSE) {
342 $success = FALSE;
343 }
344 }
345 break;
346 case self::LOCKING_METHOD_FLOCK:
347 if (is_resource($this->filePointer)) {
348 if (flock($this->filePointer, LOCK_UN) === FALSE) {
349 $success = FALSE;
350 }
351 fclose($this->filePointer);
352 }
353 break;
354 case self::LOCKING_METHOD_SEMAPHORE:
355 if (!@sem_release($this->resource)) {
356 $success = FALSE;
357 }
358 break;
359 case self::LOCKING_METHOD_DISABLED:
360 break;
361 default:
362 // will never be reached
363 }
364 $this->isAcquired = FALSE;
365 return $success;
366 }
367
368 /**
369 * Return the locking method which is currently used
370 *
371 * @return string Locking method
372 */
373 public function getMethod() {
374 return $this->method;
375 }
376
377 /**
378 * Return the ID which is currently used
379 *
380 * @return string Locking ID
381 */
382 public function getId() {
383 return $this->id;
384 }
385
386 /**
387 * Return the resource which is currently used.
388 * Depending on the locking method this can be a filename or a semaphore resource.
389 *
390 * @return mixed Locking resource (filename as string or semaphore as resource)
391 */
392 public function getResource() {
393 return $this->resource;
394 }
395
396 /**
397 * Return the local status of a lock
398 *
399 * @return bool Returns TRUE if lock is acquired by this process, FALSE otherwise
400 */
401 public function getLockStatus() {
402 return $this->isAcquired;
403 }
404
405 /**
406 * Return the global status of the lock
407 *
408 * @return bool Returns TRUE if the lock is locked by either this or another process, FALSE otherwise
409 */
410 public function isLocked() {
411 $result = FALSE;
412 switch ($this->method) {
413 case self::LOCKING_METHOD_SIMPLE:
414 if (file_exists($this->resource)) {
415 $maxExecutionTime = (int)ini_get('max_execution_time');
416 $maxAge = time() - ($maxExecutionTime ?: 120);
417 if (@filectime($this->resource) < $maxAge) {
418 @unlink($this->resource);
419 $this->sysLog('Unlinking stale lockfile');
420 } else {
421 $result = TRUE;
422 }
423 }
424 break;
425 case self::LOCKING_METHOD_FLOCK:
426 // we can't detect this reliably here, since the third parameter of flock() does not work on windows
427 break;
428 case self::LOCKING_METHOD_SEMAPHORE:
429 // no way to detect this at all, no PHP API for that
430 break;
431 case self::LOCKING_METHOD_DISABLED:
432 break;
433 default:
434 // will never be reached
435 }
436 return $result;
437 }
438
439 /**
440 * Sets the facility (extension name) for the syslog entry.
441 *
442 * @param string $syslogFacility
443 */
444 public function setSyslogFacility($syslogFacility) {
445 $this->syslogFacility = $syslogFacility;
446 }
447
448 /**
449 * Enable/ disable logging
450 *
451 * @param bool $isLoggingEnabled
452 */
453 public function setEnableLogging($isLoggingEnabled) {
454 $this->isLoggingEnabled = $isLoggingEnabled;
455 }
456
457 /**
458 * Adds a common log entry for this locking API using \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog().
459 * Example: 25-02-08 17:58 - cms: Locking [simple::0aeafd2a67a6bb8b9543fb9ea25ecbe2]: Acquired
460 *
461 * @param string $message The message to be logged
462 * @param int $severity Severity - 0 is info (default), 1 is notice, 2 is warning, 3 is error, 4 is fatal error
463 * @return void
464 */
465 public function sysLog($message, $severity = 0) {
466 if ($this->isLoggingEnabled) {
467 GeneralUtility::sysLog('Locking [' . $this->method . '::' . $this->id . ']: ' . trim($message), $this->syslogFacility, $severity);
468 }
469 }
470
471 /**
472 * Tests if the directory for simple locks is available.
473 * If not, the directory will be created. The lock path is usually
474 * below typo3temp, typo3temp itself should exist already
475 *
476 * @return void
477 * @throws \RuntimeException If path couldn't be created.
478 */
479 protected function createPathIfNeeded() {
480 $path = PATH_site . self::FILE_LOCK_FOLDER;
481 if (!is_dir($path)) {
482 // Not using mkdir_deep on purpose here, if typo3temp itself
483 // does not exist, this issue should be solved on a different
484 // level of the application.
485 if (!GeneralUtility::mkdir($path)) {
486 throw new \RuntimeException('Cannot create directory ' . $path, 1395140007);
487 }
488 }
489 if (!is_writable($path)) {
490 throw new \RuntimeException('Cannot write to directory ' . $path, 1396278700);
491 }
492 $this->resource = $path . $this->id;
493 }
494 }