Issues (836)

framework/mutex/FileMutex.php (5 issues)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://www.yiiframework.com/license/
6
 */
7
8
namespace yii\mutex;
9
10
use Yii;
11
use yii\base\InvalidConfigException;
12
use yii\helpers\FileHelper;
13
14
/**
15
 * FileMutex implements mutex "lock" mechanism via local file system files.
16
 *
17
 * This component relies on PHP `flock()` function.
18
 *
19
 * Application configuration example:
20
 *
21
 * ```php
22
 * [
23
 *     'components' => [
24
 *         'mutex' => [
25
 *             'class' => 'yii\mutex\FileMutex'
26
 *         ],
27
 *     ],
28
 * ]
29
 * ```
30
 *
31
 * > Note: this component can maintain the locks only for the single web server,
32
 * > it probably will not suffice in case you are using cloud server solution.
33
 *
34
 * > Warning: due to `flock()` function nature this component is unreliable when
35
 * > using a multithreaded server API like ISAPI.
36
 *
37
 * @see Mutex
38
 *
39
 * @author resurtm <[email protected]>
40
 * @since 2.0
41
 */
42
class FileMutex extends Mutex
43
{
44
    use RetryAcquireTrait;
45
46
    /**
47
     * @var string the directory to store mutex files. You may use [path alias](guide:concept-aliases) here.
48
     * Defaults to the "mutex" subdirectory under the application runtime path.
49
     */
50
    public $mutexPath = '@runtime/mutex';
51
    /**
52
     * @var int|null the permission to be set for newly created mutex files.
53
     * This value will be used by PHP chmod() function. No umask will be applied.
54
     * If not set, the permission will be determined by the current environment.
55
     */
56
    public $fileMode;
57
    /**
58
     * @var int the permission to be set for newly created directories.
59
     * This value will be used by PHP chmod() function. No umask will be applied.
60
     * Defaults to 0775, meaning the directory is read-writable by owner and group,
61
     * but read-only for other users.
62
     */
63
    public $dirMode = 0775;
64
    /**
65
     * @var bool|null whether file handling should assume a Windows file system.
66
     * This value will determine how [[releaseLock()]] goes about deleting the lock file.
67
     * If not set, it will be determined by checking the DIRECTORY_SEPARATOR constant.
68
     * @since 2.0.16
69
     */
70
    public $isWindows;
71
72
    /**
73
     * @var resource[] stores all opened lock files. Keys are lock names and values are file handles.
74
     */
75
    private $_files = [];
76
77
78
    /**
79
     * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like
80
     * operating systems.
81
     * @throws InvalidConfigException
82
     */
83 16
    public function init()
84
    {
85 16
        parent::init();
86 16
        $this->mutexPath = Yii::getAlias($this->mutexPath);
0 ignored issues
show
Documentation Bug introduced by
It seems like Yii::getAlias($this->mutexPath) can also be of type false. However, the property $mutexPath is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
87 16
        if (!is_dir($this->mutexPath)) {
0 ignored issues
show
It seems like $this->mutexPath can also be of type false; however, parameter $filename of is_dir() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

87
        if (!is_dir(/** @scrutinizer ignore-type */ $this->mutexPath)) {
Loading history...
88 1
            FileHelper::createDirectory($this->mutexPath, $this->dirMode, true);
0 ignored issues
show
It seems like $this->mutexPath can also be of type false; however, parameter $path of yii\helpers\BaseFileHelper::createDirectory() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
            FileHelper::createDirectory(/** @scrutinizer ignore-type */ $this->mutexPath, $this->dirMode, true);
Loading history...
89
        }
90 16
        if ($this->isWindows === null) {
91 16
            $this->isWindows = DIRECTORY_SEPARATOR === '\\';
92
        }
93
    }
94
95
    /**
96
     * Acquires lock by given name.
97
     * @param string $name of the lock to be acquired.
98
     * @param int $timeout time (in seconds) to wait for lock to become released.
99
     * @return bool acquiring result.
100
     */
101 16
    protected function acquireLock($name, $timeout = 0)
102
    {
103 16
        $filePath = $this->getLockFilePath($name);
104 16
        return $this->retryAcquire($timeout, function () use ($filePath, $name) {
105 16
            $file = fopen($filePath, 'w+');
106 16
            if ($file === false) {
107
                return false;
108
            }
109
110 16
            if ($this->fileMode !== null) {
111
                @chmod($filePath, $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

111
                /** @scrutinizer ignore-unhandled */ @chmod($filePath, $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...
112
            }
113
114 16
            if (!flock($file, LOCK_EX | LOCK_NB)) {
115 4
                fclose($file);
116 4
                return false;
117
            }
118
119
            // Under unix we delete the lock file before releasing the related handle. Thus it's possible that we've acquired a lock on
120
            // a non-existing file here (race condition). We must compare the inode of the lock file handle with the inode of the actual lock file.
121
            // If they do not match we simply continue the loop since we can assume the inodes will be equal on the next try.
122
            // Example of race condition without inode-comparison:
123
            // Script A: locks file
124
            // Script B: opens file
125
            // Script A: unlinks and unlocks file
126
            // Script B: locks handle of *unlinked* file
127
            // Script C: opens and locks *new* file
128
            // In this case we would have acquired two locks for the same file path.
129 16
            if (DIRECTORY_SEPARATOR !== '\\' && fstat($file)['ino'] !== @fileinode($filePath)) {
130
                clearstatcache(true, $filePath);
131
                flock($file, LOCK_UN);
132
                fclose($file);
133
                return false;
134
            }
135
136 16
            $this->_files[$name] = $file;
137 16
            return true;
138 16
        });
139
    }
140
141
    /**
142
     * Releases lock by given name.
143
     * @param string $name of the lock to be released.
144
     * @return bool release result.
145
     */
146 16
    protected function releaseLock($name)
147
    {
148 16
        if (!isset($this->_files[$name])) {
149 7
            return false;
150
        }
151
152 16
        if ($this->isWindows) {
153
            // Under windows it's not possible to delete a file opened via fopen (either by own or other process).
154
            // That's why we must first unlock and close the handle and then *try* to delete the lock file.
155
            flock($this->_files[$name], LOCK_UN);
156
            fclose($this->_files[$name]);
157
            @unlink($this->getLockFilePath($name));
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

157
            /** @scrutinizer ignore-unhandled */ @unlink($this->getLockFilePath($name));

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...
158
        } else {
159
            // Under unix it's possible to delete a file opened via fopen (either by own or other process).
160
            // That's why we must unlink (the currently locked) lock file first and then unlock and close the handle.
161 16
            unlink($this->getLockFilePath($name));
162 16
            flock($this->_files[$name], LOCK_UN);
163 16
            fclose($this->_files[$name]);
164
        }
165
166 16
        unset($this->_files[$name]);
167 16
        return true;
168
    }
169
170
    /**
171
     * Generate path for lock file.
172
     * @param string $name
173
     * @return string
174
     * @since 2.0.10
175
     */
176 16
    protected function getLockFilePath($name)
177
    {
178 16
        return $this->mutexPath . DIRECTORY_SEPARATOR . md5($name) . '.lock';
179
    }
180
}
181