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