Passed
Push — master ( e46a83...630973 )
by Alexander
20:03 queued 17:27
created

FileMutex::acquireLock()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 39
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 9.0146

Importance

Changes 0
Metric Value
cc 6
eloc 15
c 0
b 0
f 0
nc 7
nop 1
dl 0
loc 39
ccs 9
cts 16
cp 0.5625
crap 9.0146
rs 9.2222
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Mutex\File;
6
7
use Yiisoft\Files\FileHelper;
8
use Yiisoft\Mutex\Mutex;
9
10
use function chmod;
11
use function clearstatcache;
12
use function fclose;
13
use function fileinode;
14
use function flock;
15
use function fopen;
16
use function fstat;
17
use function md5;
18
use function unlink;
19
20
/**
21
 * FileMutex implements mutex "lock" mechanism via local file system files.
22
 *
23
 * This component relies on PHP {@see flock()} function.
24
 *
25
 * > Note: this component can maintain the locks only for the single web server,
26
 * > it probably will not suffice in case you are using cloud server solution.
27
 *
28
 * > Warning: due to {@see flock()} function nature this component is unreliable when
29
 * > using a multithreaded server API like ISAPI.
30
 */
31
final class FileMutex extends Mutex
32
{
33
    private string $lockFilePath;
34
    private ?int $fileMode;
35
36
    /**
37
     * @var closed-resource|resource|null Stores opened lock file resource.
0 ignored issues
show
Documentation Bug introduced by
The doc comment closed-resource|resource|null at position 0 could not be parsed: Unknown type name 'closed-resource' at position 0 in closed-resource|resource|null.
Loading history...
38
     */
39
    private $lockResource = null;
40
41
    /**
42
     * @param string $name Mutex name.
43
     * @param string $mutexPath The directory to store mutex files.
44
     * @param int $directoryMode The permission to be set for newly created directories.
45
     * This value will be used by PHP {@see chmod()} function. No umask will be applied. Defaults to 0775,
46
     * meaning the directory is read-writable by owner and group, but read-only for other users.
47
     * @param int|null $fileMode The permission to be set for newly created mutex files.
48
     * This value will be used by PHP {@see chmod()} function. No umask will be applied.
49
     */
50 7
    public function __construct(string $name, string $mutexPath, int $directoryMode = 0775, int $fileMode = null)
51
    {
52 7
        FileHelper::ensureDirectory($mutexPath, $directoryMode);
53 7
        $this->lockFilePath = $mutexPath . DIRECTORY_SEPARATOR . md5($name) . '.lock';
54 7
        $this->fileMode = $fileMode;
55 7
        parent::__construct(self::class, $name);
56 7
    }
57
58 7
    protected function acquireLock(int $timeout = 0): bool
59
    {
60 7
        $resource = fopen($this->lockFilePath, 'wb+');
61
62 7
        if ($resource === false) {
63
            return false;
64
        }
65
66 7
        if ($this->fileMode !== null) {
67 1
            @chmod($this->lockFilePath, $this->fileMode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). 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 ignore-unhandled  annotation

67
            /** @scrutinizer ignore-unhandled */ @chmod($this->lockFilePath, $this->fileMode);

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.');
}
Loading history...
68
        }
69
70 7
        if (!flock($resource, LOCK_EX | LOCK_NB)) {
71
            fclose($resource);
72
            return false;
73
        }
74
75
        // Under unix, we delete the lock file before releasing the related handle. Thus, it's possible that we've
76
        // acquired a lock on a non-existing file here (race condition). We must compare the inode of the lock file
77
        // handle with the inode of the actual lock file.
78
        // If they do not match we simply continue the loop since we can assume the inodes will be equal on the
79
        // next try.
80
        // Example of race condition without inode-comparison:
81
        // Script A: locks file
82
        // Script B: opens file
83
        // Script A: unlinks and unlocks file
84
        // Script B: locks handle of *unlinked* file
85
        // Script C: opens and locks *new* file
86
        // In this case we would have acquired two locks for the same file path.
87 7
        if (DIRECTORY_SEPARATOR !== '\\' && fstat($resource)['ino'] !== @fileinode($this->lockFilePath)) {
88
            clearstatcache(true, $this->lockFilePath);
89
            flock($resource, LOCK_UN);
90
            fclose($resource);
91
92
            return false;
93
        }
94
95 7
        $this->lockResource = $resource;
96 7
        return true;
97
    }
98
99 7
    protected function releaseLock(): bool
100
    {
101 7
        if (!is_resource($this->lockResource)) {
102
            return false;
103
        }
104
105 7
        if (DIRECTORY_SEPARATOR === '\\') {
106
            // Under windows, it's not possible to delete a file opened via fopen (either by own or other process).
107
            // That's why we must first unlock and close the handle and then *try* to delete the lock file.
108
            flock($this->lockResource, LOCK_UN);
109
            fclose($this->lockResource);
110
            @unlink($this->lockFilePath);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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 ignore-unhandled  annotation

110
            /** @scrutinizer ignore-unhandled */ @unlink($this->lockFilePath);

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.');
}
Loading history...
111
        } else {
112
            // Under unix, it's possible to delete a file opened via fopen (either by own or other process).
113
            // That's why we must unlink (the currently locked) lock file first and then unlock and close the handle.
114 7
            @unlink($this->lockFilePath);
115 7
            flock($this->lockResource, LOCK_UN);
116 7
            fclose($this->lockResource);
117
        }
118
119 7
        $this->lockResource = null;
120 7
        return true;
121
    }
122
}
123