Completed
Push — master ( 69451c...ec8e6e )
by Anatoliy
10:56
created

DenyMultiplyRun   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 409
Duplicated Lines 0 %

Test Coverage

Coverage 85.5%

Importance

Changes 0
Metric Value
wmc 45
dl 0
loc 409
c 0
b 0
f 0
ccs 112
cts 131
cp 0.855
rs 8.3673

16 Methods

Rating   Name   Duplication   Size   Complexity  
A truncatePidFile() 0 10 3
A errorHandle() 0 10 1
A preparePidDir() 0 8 4
A openPidFile() 0 16 3
A deletePidFile() 0 20 3
B setPidFile() 0 43 6
B createPidFile() 0 27 3
A setPidIntoFile() 0 7 2
A __construct() 0 2 1
B validatePid() 0 30 5
A startErrorHandle() 0 5 1
A unlockPidFile() 0 5 2
A getPidFromFile() 0 15 2
A checkRunnedPid() 0 13 3
B closePidFile() 0 37 4
A lockPidFile() 0 18 2

How to fix   Complexity   

Complex Class

Complex classes like DenyMultiplyRun often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DenyMultiplyRun, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types = 1);
3
4
namespace DanchukAS\DenyMultiplyRun;
5
6
use DanchukAS\DenyMultiplyRun\Exception\CloseFileFail;
7
use DanchukAS\DenyMultiplyRun\Exception\ConvertPidFail;
8
use DanchukAS\DenyMultiplyRun\Exception\DeleteFileFail;
9
use DanchukAS\DenyMultiplyRun\Exception\FileExisted;
10
use DanchukAS\DenyMultiplyRun\Exception\LockFileFail;
11
use DanchukAS\DenyMultiplyRun\Exception\OpenFileFail;
12
use DanchukAS\DenyMultiplyRun\Exception\PidBiggerMax;
13
use DanchukAS\DenyMultiplyRun\Exception\PidFileEmpty;
14
use DanchukAS\DenyMultiplyRun\Exception\ProcessExisted;
15
use DanchukAS\DenyMultiplyRun\Exception\ReadFileFail;
16
17
18
/**
19
 * Class denyMultiplyRun
20
 * Забороняє паралельний запуск скрипта
21
 *
22
 * @todo: extract work with file to another lib.
23
 *
24
 * @package DanchukAS\DenyMultiplyRun
25
 */
26
class DenyMultiplyRun
27
{
28
    /**
29
     * Для перехвата помилок що не кидають ексепшини.
30
     *
31
     * @var \Throwable
32
     */
33
    private static $lastError;
34
35
36
    /**
37
     * DenyMultiplyRun constructor.
38
     * Унеможливлює створення обєктів цього класу.
39
     * Даний клас лише для статичного визова методів.
40
     */
41
    private function __construct()
42
    {
43
    }
44
45
46
    /**
47
     * Унеможливлює паралельний запуск ще одного процеса pid-файл якого ідентичний.
48
     *
49
     * Створює файл в якому число-ідентифікатор процеса ОС під яким працює даний код.
50
     * Якщо файл існує і процеса з номером що в файлі записаний не існує -
51
     * пробує записати теперішній ідентифікатор процеса ОС під яким працює даний код.
52
     * В усіх інших випадках кидає відповідні виключення.
53
     *
54
     * @param string $pidFilePath Шлях до файла. Вимоги: користувач під яким запущений
55
     *                            даний код має мати право на створення, читання і зміну
56
     *                            даного файла.
57 8
     */
58
    public static function setPidFile(string $pidFilePath)
59 8
    {
60
        self::preparePidDir($pidFilePath);
61
62 8
        try {
63 2
            $file_resource = self::createPidFile($pidFilePath);
64 7
            $pid_file_existed = false;
65 7
        } catch (FileExisted $exception) {
66 7
            $file_resource = self::openPidFile($pidFilePath);
67
            $pid_file_existed = true;
68
        }
69 8
70
        self::lockPidFile($file_resource);
71
72
        try {
73 7
            // оголошено тут щоб шторм не ругався нижче, бо не може зрозуміти що змінна вже оголошена
74 7
            $prev_pid = null;
75
            if ($pid_file_existed) {
76 6
                try {
77 3
                    $prev_pid = self::getPidFromFile($file_resource);
78 5
                    self::checkRunnedPid($prev_pid);
79
                } catch (PidFileEmpty $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
80 2
                }
81
                self::truncatePidFile($file_resource);
82
            }
83 4
84 4
            $self_pid = getmypid();
85
            self::setPidIntoFile($self_pid, $file_resource);
86 4
87 2
            if ($pid_file_existed) {
88 1
                $message_reason = is_null($prev_pid)
89 2
                    ? ", but file empty."
90 2
                    : ", but process with contained ID($prev_pid) in it is not exist.";
91 2
                $message = "pid-file exist" . $message_reason
92
                    . " pid-file updated with pid this process: " . $self_pid;
93 2
94
                trigger_error($message, E_USER_NOTICE);
95 4
            }
96
        } finally {
97 7
            try {
98 4
                self::unlockPidFile($file_resource);
99 7
            } finally {
100
                self::closePidFile($file_resource);
101
            }
102
        }
103 4
104
    }
0 ignored issues
show
Coding Style introduced by
Function closing brace must go on the next line following the body; found 1 blank lines before brace
Loading history...
105
106
    /**
107
     * @param string $pidFilePath
108
     *
109
     * @throws \Exception
110 8
     */
111
    private static function preparePidDir($pidFilePath)
112 8
    {
113 8
        $pid_dir = dirname($pidFilePath);
114
115
        if ("" !== $pid_dir && !is_dir($pid_dir)) {
116
            $created_pid_dir = @mkdir($pid_dir, 0777, true);
117
            if (false === $created_pid_dir) {
118
                throw new \Exception('Директорія відсутня і неможливо створити: ' . $pid_dir);
119 8
            }
120
        }
121
    }
122
123
    /**
124
     * @param string $pidFilePath
125
     * @return resource
126
     * @throws FileExisted
127 8
     * @throws \Exception
128
     */
129
    private static function createPidFile($pidFilePath)
130
    {
131 8
        // перехоплювач на 1 команду, щоб в разі потреби потім дізнатись причину несправності.
132
        // помилку в записує в self::$lastError
133
        self::startErrorHandle();
134
135 8
        // собачка потрібна щоб не засоряти логи.
136
        /** @noinspection PhpUsageOfSilenceOperatorInspection */
137
        $pid_file_handle = @fopen($pidFilePath, 'x');
138 8
139
        // Відновлюєм попередній обробник наче нічого і не робили.
140
        restore_error_handler();
141 8
142
        // файл не створений. сталась помилка
143
        if (!is_null(self::$lastError)) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
144 7
145
            // Файла і нема і не створився - повідомляєм про несправність проекта.
146
            if (!is_file($pidFilePath)) {
147
                throw new self::$lastError;
148
            }
149 7
150
            // Файл вже існує, тому не створився.
151
            throw new FileExisted($pidFilePath);
152
        }
153 2
154
        // файл створений успішно.
155
        return $pid_file_handle;
156
    }
157
158
    private static function startErrorHandle()
159
    {
160
        set_error_handler([__CLASS__, 'errorHandle']);
161 7
162
        self::$lastError = null;
163 7
    }
164 7
165
    /**
166
     * @param string $pidFilePath
167 7
     * @return resource
168
     * @throws \Exception
169
     */
170
    private static function openPidFile($pidFilePath)
171
    {
172
        self::startErrorHandle();
173
        try {
174
            $pid_file_handle = @fopen($pidFilePath, 'r+');
175 8
        } catch (\Throwable $error) {
176
            self::$lastError = $error;
177 8
        } finally {
178 8
            restore_error_handler();
179 1
        }
180
181
        if (!is_null(self::$lastError)) {
182
            throw new OpenFileFail((string)self::$lastError);
183 1
        }
184
185
        return $pid_file_handle;
186
    }
187 1
188
    /**
189
     * @param $pidFileResource
190 1
     *
191
     * @throws \Exception
192 1
     */
193
    private static function lockPidFile($pidFileResource)
194 7
    {
195
        $locked = flock($pidFileResource, LOCK_EX | LOCK_NB);
196
        if (false === $locked) {
197
            $error = self::$lastError;
198
199
            // перехоплювач на 1 команду, щоб в разі потреби потім дізнатись причину несправності.
200
            // помилку в записує в self::$lastError
201 6
            self::startErrorHandle();
202
203
            // собачка потрібна щоб не засоряти логи.
204
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
205
            $resource_data = @stream_get_meta_data($pidFileResource);
206 6
207
            // Відновлюєм попередній обробник наче нічого і не робили.
208
            restore_error_handler();
209 6
210
            throw new LockFileFail($resource_data['uri'] . ' - ' . $error);
211
        }
212
    }
213 6
214
    /**
215 3
     * @param $pidFileResource Дескриптор файла доступного для читання в якому знаходиться PID.
216
     * @return int PID з файла
217
     * @throws \Exception
218
     */
219
    private static function getPidFromFile($pidFileResource)
220
    {
221
        // Розмір PID (int в ОС) навряд буде більший ніж розмір int в PHP.
222
        // Зазвичай PID має до 5 цифр.
223
        // @todo: if error - warning, error_handler, ...
224 6
        $pid_from_file = fread($pidFileResource, 64);
225
226
227 6
        if (false === $pid_from_file) {
228 1
            throw new ReadFileFail("pid-файл є, але прочитати що в ньому не вдалось.");
229
        }
230
231 5
        $pid_int = self::validatePid($pid_from_file);
232
233
        return $pid_int;
234
    }
235
236 5
    /**
237 5
     * @param string $pid_from_file
238 5
     * @return int
239 5
     * @throws PidBiggerMax
240 1
     * @throws PidFileEmpty
241 1
     */
242
    private static function validatePid(string $pid_from_file): int
243
    {
244
        // На випадок коли станеться виліт скрипта після створення файла і до запису ІД.
245
        if ('' === $pid_from_file) {
246
            throw new PidFileEmpty();
247 4
        }
248 1
249 1
        $pid_int = (int) $pid_from_file;
250 1
251
        // verify available PID in file.
252
        // if PID not available - why it happens ?
253 3
        // For *nix system
254
        $pid_max_storage = "/proc/sys/kernel/pid_max";
255
        if (file_exists($pid_max_storage)) {
256
            $pid_max = (int) file_get_contents($pid_max_storage);
257
            if ($pid_max < $pid_int) {
258
                $message = "PID in file has unavailable value: $pid_int. In /proc/sys/kernel/pid_max set $pid_max.";
259
                throw new PidBiggerMax($message);
260
            }
261 3
        }
262
263
        // verify converting. (PHP_MAX_INT)
264
        // verify PID in file is right (something else instead ciphers).
265
        if ("{$pid_int}" !== $pid_from_file) {
266
            $message = "pid_int({$pid_int}) !== pid_string($pid_from_file)"
267 3
                . ", or pid_string($pid_from_file) is not Process ID)";
268
            throw new ConvertPidFail($message);
269
        }
270
271 3
        return $pid_int;
272
    }
273 2
274
    /**
275 1
     * @param int $pid
276
     *
277
     * @throws ProcessExisted
278
     */
279
    private static function checkRunnedPid($pid)
280
    {
281
        if (
282 2
            // Посилає сигнал процесу щоб дізнатись чи він існує.
283
            // Якщо true - точно існує.
284 2
            // якщо false - процес може і бути, але запущений під іншим користувачем або інші ситуації.
285 2
            true === posix_kill($pid, 0)
286
            // 3 = No such process (тестувалось в Ubuntu 14, FreeBSD 9. В інших ОС можуть відрізнятись)
287
            // Якщо процеса точно в даний момент нема такого.
288
            // Для визова цієї функції необхідний попередній визов posix_kill.
289 2
            || posix_get_last_error() !== 3
290 2
        ) {
291
            throw new ProcessExisted($pid);
292
        }
293 2
    }
294
295
    /**
296
     * @param $pidFileResource
297
     *
298
     * @throws \Exception
299
     */
300
    private static function truncatePidFile($pidFileResource)
301 4
    {
302
        $truncated = ftruncate($pidFileResource, 0);
303 4
        if (false === $truncated) {
304 4
            throw new \Exception("не вдалось очистити pid-файл.");
305 4
        }
306 4
307
        $cursor_to_begin = rewind($pidFileResource);
308
        if (!$cursor_to_begin) {
309 4
            throw new \Exception("не вдалось перемістити курсор на початок pid-файла.");
310
        }
311
    }
312
313
    /**
314 7
     * @param int $self_pid
315
     * @param $pidFileResource
316 7
     *
317 7
     * @throws \Exception
318
     */
319
    private static function setPidIntoFile($self_pid, $pidFileResource)
320 7
    {
321
        $self_pid = '' . $self_pid;
322
        $pid_length = strlen($self_pid);
323
        $write_length = fwrite($pidFileResource, $self_pid, $pid_length);
324
        if ($write_length !== $pid_length) {
325
            throw new \Exception("не вдось записати pid в pid-файл. Записано $write_length байт замість $pid_length");
326
        }
327 7
    }
328
329
    /**
330
     * @param $pidFileResource
331 7
     */
332
    private static function unlockPidFile($pidFileResource)
333
    {
334
        $unlocked = flock($pidFileResource, LOCK_UN | LOCK_NB);
335 7
        if (false === $unlocked) {
336
            trigger_error("не вдось розблокувати pid-файл.");
337
        }
338 7
    }
339
340 7
    /**
341
     * @param $pidFileResource
342
     *
343
     * @throws CloseFileFail
344
     */
345
    private static function closePidFile($pidFileResource)
346
    {
347
        // перехоплювач на 1 команду, щоб в разі потреби потім дізнатись причину несправності.
348
        // помилку в записує в self::$lastError
349
        self::startErrorHandle();
350
351
        try {
352
            // собачка потрібна щоб не засоряти логи.
353
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
354
            $closed = @fclose($pidFileResource);
0 ignored issues
show
Unused Code introduced by
The assignment to $closed is dead and can be removed.
Loading history...
355
        } catch (\Throwable $error) {
356
            self::$lastError = $error;
357 7
        } finally {
358
            // Відновлюєм попередній обробник наче нічого і не робили.
359
            restore_error_handler();
360
        }
361
362
363
        if (!is_null(self::$lastError)) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
364
365
            $file_close_error = self::$lastError;
366 3
367
            // перехоплювач на 1 команду, щоб в разі потреби потім дізнатись причину несправності.
368
            // помилку в записує в self::$lastError
369
            self::startErrorHandle();
370 3
371
            try {
372
                // собачка потрібна щоб не засоряти логи.
373
                /** @noinspection PhpUsageOfSilenceOperatorInspection */
374
                $resource_data = @stream_get_meta_data($pidFileResource);
375 3
            } catch (\Throwable $error) {
376
                $resource_data = ['uri' => ''];
377
            } finally {
378
                // Відновлюєм попередній обробник наче нічого і не робили.
379
                restore_error_handler();
380 3
            }
381
            throw new CloseFileFail($resource_data['uri'], 457575, $file_close_error);
382 3
        }
383
    }
384
385 3
    /**
386 1
     * Відключає встановлену заборону паралельного запуска у яких спільний $pidFilePath
387
     * @todo добавити перевірку що цей файл ще для цього процеса,
388
     * може цей файл вже був видалений вручну, і створений іншим процесом.
389
     *
390 2
     * @param string $pidFilePath
391
     *
392
     * @throws DeleteFileFail
393
     */
394
    public static function deletePidFile($pidFilePath)
395
    {
396
        // перехоплювач на 1 команду, щоб в разі потреби потім дізнатись причину несправності.
397
        // помилку в записує в self::$lastError
398
        self::startErrorHandle();
399
400 9
        try {
401
            // собачка потрібна щоб не засоряти логи.
402
            /** @noinspection PhpUsageOfSilenceOperatorInspection */
403
            @unlink($pidFilePath);
1 ignored issue
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

403
            /** @scrutinizer ignore-unhandled */ @unlink($pidFilePath);

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...
404 9
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
405
        } catch (\Throwable $error) {
406 9
            self::$lastError = $error;
407
        } finally {
408
            // Відновлюєм попередній обробник наче нічого і не робили.
409 9
            restore_error_handler();
410
        }
411
412
        if (!is_null(self::$lastError)) {
413
            throw new DeleteFileFail(self::$lastError);
414
        }
415
    }
416
417
    /**
418
     * @param int $messageType
419
     * @param string $messageText
420
     * @param string $messageFile
421
     * @param int $messageLine
422
     *
423
     * @return bool
424
     */
425
    public static function errorHandle(int $messageType, string $messageText, string $messageFile, int $messageLine)
426
    {
427
        // добавляємо лише інформацію яка є.
428
        // все інше добавляти має обробник самого проекта.
429
        $message = "[$messageType] $messageText in $messageFile on line $messageLine";
430
431
        self::$lastError = new \Exception($message);
432
433
        // Перехопити перехопили, кидаєм далі обробляти.
434
        return false;
435
    }
436
437
438
}
439
440