[TASK] Raise severity for stale lock file
[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 * @deprecated since TYPO3 CMS 7, will be removed with TYPO3 CMS 8
96 */
97 public function __construct($id, $method = self::LOCKING_METHOD_SIMPLE, $loops = 0, $step = 0) {
98 GeneralUtility::logDeprecatedFunction();
99 // Force ID to be string
100 $id = (string)$id;
101 if ((int)$loops) {
102 $this->loops = (int)$loops;
103 }
104 if ((int)$step) {
105 $this->step = (int)$step;
106 }
107 if ($method === '' && isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'])) {
108 $method = (string)$GLOBALS['TYPO3_CONF_VARS']['SYS']['lockingMode'];
109 }
110
111 switch ($method) {
112 case self::LOCKING_METHOD_SIMPLE:
113 // intended fall through
114 case self::LOCKING_METHOD_FLOCK:
115 $this->id = md5($id);
116 $this->createPathIfNeeded();
117 break;
118 case self::LOCKING_METHOD_SEMAPHORE:
119 $this->id = abs(crc32($id));
120 break;
121 case self::LOCKING_METHOD_DISABLED:
122 break;
123 default:
124 throw new \InvalidArgumentException('No such locking method "' . $method . '"', 1294586097);
125 }
126 $this->method = $method;
127 }
128
129 /**
130 * Destructor:
131 * Releases lock automatically when instance is destroyed and release resources
132 */
133 public function __destruct() {
134 $this->release();
135 switch ($this->method) {
136 case self::LOCKING_METHOD_FLOCK:
137 if (
138 GeneralUtility::isAllowedAbsPath($this->resource)
139 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
140 ) {
141 @unlink($this->resource);
142 }
143 break;
144 case self::LOCKING_METHOD_SEMAPHORE:
145 @sem_remove($this->resource);
146 break;
147 default:
148 // do nothing
149 }
150 }
151
152 /**
153 * Tries to allocate the semaphore
154 *
155 * @return void
156 * @throws \RuntimeException
157 */
158 protected function getSemaphore() {
159 $this->resource = sem_get($this->id, 1);
160 if ($this->resource === FALSE) {
161 throw new \RuntimeException('Unable to get semaphore with id ' . $this->id, 1313828196);
162 }
163 }
164
165 /**
166 * Acquire a lock and return when successful.
167 *
168 * It is important to know that the lock will be acquired in any case, even if the request was blocked first.
169 * Therefore, the lock needs to be released in every situation.
170 *
171 * @return bool Returns TRUE if lock could be acquired without waiting, FALSE otherwise.
172 * @throws \RuntimeException
173 * @deprecated since 6.2 - will be removed two versions later; use new API instead
174 */
175 public function acquire() {
176 // @todo refactor locking in TSFE to use the new API, then this call can be logged
177 // GeneralUtility::logDeprecatedFunction();
178
179 // Default is TRUE, which means continue without caring for other clients.
180 // In the case of TYPO3s cache management, this has no negative effect except some resource overhead.
181 $noWait = FALSE;
182 $isAcquired = FALSE;
183 switch ($this->method) {
184 case self::LOCKING_METHOD_SIMPLE:
185 if (file_exists($this->resource)) {
186 $this->sysLog('Waiting for a different process to release the lock');
187 $maxExecutionTime = (int)ini_get('max_execution_time');
188 $maxAge = time() - ($maxExecutionTime ?: 120);
189 if (@filectime($this->resource) < $maxAge) {
190 @unlink($this->resource);
191 $this->sysLog('Unlinking stale lockfile', GeneralUtility::SYSLOG_SEVERITY_WARNING);
192 }
193 }
194 for ($i = 0; $i < $this->loops; $i++) {
195 $filePointer = @fopen($this->resource, 'x');
196 if ($filePointer !== FALSE) {
197 fclose($filePointer);
198 GeneralUtility::fixPermissions($this->resource);
199 $this->sysLog('Lock acquired');
200 $noWait = $i === 0;
201 $isAcquired = TRUE;
202 break;
203 }
204 usleep($this->step * 1000);
205 }
206 if (!$isAcquired) {
207 throw new \RuntimeException('Lock file could not be created', 1294586098);
208 }
209 break;
210 case self::LOCKING_METHOD_FLOCK:
211 $this->filePointer = fopen($this->resource, 'c');
212 if ($this->filePointer === FALSE) {
213 throw new \RuntimeException('Lock file could not be opened', 1294586099);
214 }
215 // Lock without blocking
216 if (flock($this->filePointer, LOCK_EX | LOCK_NB)) {
217 $noWait = TRUE;
218 } elseif (flock($this->filePointer, LOCK_EX)) {
219 // Lock with blocking (waiting for similar locks to become released)
220 $noWait = FALSE;
221 } else {
222 throw new \RuntimeException('Could not lock file "' . $this->resource . '"', 1294586100);
223 }
224 $isAcquired = TRUE;
225 break;
226 case self::LOCKING_METHOD_SEMAPHORE:
227 $this->getSemaphore();
228 while (!$isAcquired) {
229 if (@sem_acquire($this->resource)) {
230 // Unfortunately it is not possible to find out if the request has blocked,
231 // as sem_acquire will block until we get the resource.
232 // So we do not set $noWait here at all
233 $isAcquired = TRUE;
234 }
235 }
236 break;
237 case self::LOCKING_METHOD_DISABLED:
238 break;
239 default:
240 // will never be reached
241 }
242 $this->isAcquired = $isAcquired;
243 return $noWait;
244 }
245
246 /**
247 * Try to acquire an exclusive lock
248 *
249 * @throws \RuntimeException
250 * @return bool Returns TRUE if the lock was acquired successfully
251 */
252 public function acquireExclusiveLock() {
253 if ($this->isAcquired) {
254 return TRUE;
255 }
256 $this->isAcquired = FALSE;
257 switch ($this->method) {
258 case self::LOCKING_METHOD_SIMPLE:
259 if (file_exists($this->resource)) {
260 $this->sysLog('Waiting for a different process to release the lock');
261 $maxExecutionTime = (int)ini_get('max_execution_time');
262 $maxAge = time() - ($maxExecutionTime ?: 120);
263 if (@filectime($this->resource) < $maxAge) {
264 @unlink($this->resource);
265 $this->sysLog('Unlinking stale lockfile', GeneralUtility::SYSLOG_SEVERITY_WARNING);
266 }
267 }
268 for ($i = 0; $i < $this->loops; $i++) {
269 $filePointer = @fopen($this->resource, 'x');
270 if ($filePointer !== FALSE) {
271 fclose($filePointer);
272 GeneralUtility::fixPermissions($this->resource);
273 $this->sysLog('Lock acquired');
274 $this->isAcquired = TRUE;
275 break;
276 }
277 usleep($this->step * 1000);
278 }
279 break;
280 case self::LOCKING_METHOD_FLOCK:
281 $this->filePointer = fopen($this->resource, 'c');
282 if ($this->filePointer === FALSE) {
283 throw new \RuntimeException('Lock file could not be opened', 1294586099);
284 }
285 if (flock($this->filePointer, LOCK_EX)) {
286 $this->isAcquired = TRUE;
287 }
288 break;
289 case self::LOCKING_METHOD_SEMAPHORE:
290 $this->getSemaphore();
291 if (@sem_acquire($this->resource)) {
292 $this->isAcquired = TRUE;
293 }
294 break;
295 case self::LOCKING_METHOD_DISABLED:
296 break;
297 default:
298 // will never be reached
299 }
300 return $this->isAcquired;
301 }
302
303 /**
304 * Try to acquire a shared lock
305 *
306 * (Only works for the flock() locking method currently)
307 *
308 * @return bool Returns TRUE if the lock was acquired successfully
309 * @throws \RuntimeException
310 */
311 public function acquireSharedLock() {
312 if ($this->isAcquired) {
313 return TRUE;
314 }
315 if ($this->method === self::LOCKING_METHOD_FLOCK) {
316 $this->filePointer = fopen($this->resource, 'c');
317 if ($this->filePointer === FALSE) {
318 throw new \RuntimeException('Lock file could not be opened', 1294586099);
319 }
320 if (flock($this->filePointer, LOCK_SH)) {
321 $this->isAcquired = TRUE;
322 }
323 }
324 return $this->isAcquired;
325 }
326
327 /**
328 * Release the lock
329 *
330 * @return bool Returns TRUE on success or FALSE on failure
331 */
332 public function release() {
333 if (!$this->isAcquired) {
334 return TRUE;
335 }
336 $success = TRUE;
337 switch ($this->method) {
338 case self::LOCKING_METHOD_SIMPLE:
339 if (
340 GeneralUtility::isAllowedAbsPath($this->resource)
341 && GeneralUtility::isFirstPartOfStr($this->resource, PATH_site . self::FILE_LOCK_FOLDER)
342 ) {
343 if (@unlink($this->resource) === FALSE) {
344 $success = FALSE;
345 }
346 }
347 break;
348 case self::LOCKING_METHOD_FLOCK:
349 if (is_resource($this->filePointer)) {
350 if (flock($this->filePointer, LOCK_UN) === FALSE) {
351 $success = FALSE;
352 }
353 fclose($this->filePointer);
354 }
355 break;
356 case self::LOCKING_METHOD_SEMAPHORE:
357 if (!@sem_release($this->resource)) {
358 $success = FALSE;
359 }
360 break;
361 case self::LOCKING_METHOD_DISABLED:
362 break;
363 default:
364 // will never be reached
365 }
366 $this->isAcquired = FALSE;
367 return $success;
368 }
369
370 /**
371 * Return the locking method which is currently used
372 *
373 * @return string Locking method
374 */
375 public function getMethod() {
376 return $this->method;
377 }
378
379 /**
380 * Return the ID which is currently used
381 *
382 * @return string Locking ID
383 */
384 public function getId() {
385 return $this->id;
386 }
387
388 /**
389 * Return the resource which is currently used.
390 * Depending on the locking method this can be a filename or a semaphore resource.
391 *
392 * @return mixed Locking resource (filename as string or semaphore as resource)
393 */
394 public function getResource() {
395 return $this->resource;
396 }
397
398 /**
399 * Return the local status of a lock
400 *
401 * @return bool Returns TRUE if lock is acquired by this process, FALSE otherwise
402 */
403 public function getLockStatus() {
404 return $this->isAcquired;
405 }
406
407 /**
408 * Return the global status of the lock
409 *
410 * @return bool Returns TRUE if the lock is locked by either this or another process, FALSE otherwise
411 */
412 public function isLocked() {
413 $result = FALSE;
414 switch ($this->method) {
415 case self::LOCKING_METHOD_SIMPLE:
416 if (file_exists($this->resource)) {
417 $maxExecutionTime = (int)ini_get('max_execution_time');
418 $maxAge = time() - ($maxExecutionTime ?: 120);
419 if (@filectime($this->resource) < $maxAge) {
420 @unlink($this->resource);
421 $this->sysLog('Unlinking stale lockfile', GeneralUtility::SYSLOG_SEVERITY_WARNING);
422 } else {
423 $result = TRUE;
424 }
425 }
426 break;
427 case self::LOCKING_METHOD_FLOCK:
428 // we can't detect this reliably here, since the third parameter of flock() does not work on windows
429 break;
430 case self::LOCKING_METHOD_SEMAPHORE:
431 // no way to detect this at all, no PHP API for that
432 break;
433 case self::LOCKING_METHOD_DISABLED:
434 break;
435 default:
436 // will never be reached
437 }
438 return $result;
439 }
440
441 /**
442 * Sets the facility (extension name) for the syslog entry.
443 *
444 * @param string $syslogFacility
445 */
446 public function setSyslogFacility($syslogFacility) {
447 $this->syslogFacility = $syslogFacility;
448 }
449
450 /**
451 * Enable/ disable logging
452 *
453 * @param bool $isLoggingEnabled
454 */
455 public function setEnableLogging($isLoggingEnabled) {
456 $this->isLoggingEnabled = $isLoggingEnabled;
457 }
458
459 /**
460 * Adds a common log entry for this locking API using \TYPO3\CMS\Core\Utility\GeneralUtility::sysLog().
461 * Example: 25-02-08 17:58 - cms: Locking [simple::0aeafd2a67a6bb8b9543fb9ea25ecbe2]: Acquired
462 *
463 * @param string $message The message to be logged
464 * @param int $severity Severity - 0 is info (default), 1 is notice, 2 is warning, 3 is error, 4 is fatal error
465 * @return void
466 */
467 public function sysLog($message, $severity = 0) {
468 if ($this->isLoggingEnabled) {
469 GeneralUtility::sysLog('Locking [' . $this->method . '::' . $this->id . ']: ' . trim($message), $this->syslogFacility, $severity);
470 }
471 }
472
473 /**
474 * Tests if the directory for simple locks is available.
475 * If not, the directory will be created. The lock path is usually
476 * below typo3temp, typo3temp itself should exist already
477 *
478 * @return void
479 * @throws \RuntimeException If path couldn't be created.
480 */
481 protected function createPathIfNeeded() {
482 $path = PATH_site . self::FILE_LOCK_FOLDER;
483 if (!is_dir($path)) {
484 // Not using mkdir_deep on purpose here, if typo3temp itself
485 // does not exist, this issue should be solved on a different
486 // level of the application.
487 if (!GeneralUtility::mkdir($path)) {
488 throw new \RuntimeException('Cannot create directory ' . $path, 1395140007);
489 }
490 }
491 if (!is_writable($path)) {
492 throw new \RuntimeException('Cannot write to directory ' . $path, 1396278700);
493 }
494 $this->resource = $path . $this->id;
495 }
496
497 }