Completed
Push — add_bdd ( 07502f )
by Anatoliy
07:38
created

DenyMultiplyRun::truncatePidFile()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 5.667

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
ccs 1
cts 3
cp 0.3333
cc 3
eloc 6
nc 1
nop 1
crap 5.667
1
<?php
2
declare(strict_types = 1);
3
4
namespace DanchukAS\DenyMultiplyRun;
5
6
use DanchukAS\DenyMultiplyRun\Exception\ConvertPidFail;
7
use DanchukAS\DenyMultiplyRun\Exception\DeleteFileFail;
8
use DanchukAS\DenyMultiplyRun\Exception\FileExisted;
9
use DanchukAS\DenyMultiplyRun\Exception\PidBiggerMax;
10
use DanchukAS\DenyMultiplyRun\Exception\PidFileEmpty;
11
use DanchukAS\DenyMultiplyRun\Exception\PidLessMin;
12
use DanchukAS\DenyMultiplyRun\Exception\ProcessExisted;
13
use DanchukAS\DenyMultiplyRun\Exception\ReadFileFail;
14
15
/**
16
 * Class denyMultiplyRun
17
 * Забороняє паралельний запуск скрипта
18
 *
19
 * @todo: extract work with file to another lib.
20
 *
21
 * @package DanchukAS\DenyMultiplyRun
22
 */
23
class DenyMultiplyRun
24
{
25
26
27
    /**
28
     * @var int
29
     */
30
    private static $prevPid;
31
32
33
    /**
34
     * DenyMultiplyRun constructor.
35
     * Унеможливлює створення обєктів цього класу.
36
     * Даний клас лише для статичного визова методів.
37
     */
38
    private function __construct()
39
    {
40
    }
41
42
43
    /**
44
     * Унеможливлює паралельний запуск ще одного процеса pid-файл якого ідентичний.
45
     *
46
     * Створює файл в якому число-ідентифікатор процеса ОС під яким працює даний код.
47
     * Якщо файл існує і процеса з номером що в файлі записаний не існує -
48
     * пробує записати теперішній ідентифікатор процеса ОС під яким працює даний код.
49
     * В усіх інших випадках кидає відповідні виключення.
50
     *
51
     * @param string $pidFilePath Шлях до файла. Вимоги: користувач під яким запущений
52
     *                            даний код має мати право на створення, читання і зміну
53
     *                            даного файла.
54
     * @throws \Exception
55
     */
56
    public static function setPidFile(string $pidFilePath)
57
    {
58
        File::prepareDir($pidFilePath);
59
60
        try {
61
            $file_resource = File::createPidFile($pidFilePath);
62
            $pid_file_existed = false;
63
        } catch (FileExisted $exception) {
64
            $file_resource = File::openFile($pidFilePath, 'rb+');
65
            $pid_file_existed = true;
66
        }
67
68
        File::lockPidFile($file_resource);
69
70
        try {
71
            self::safeSetPidIntoFile($pid_file_existed, $file_resource);
72
        } finally {
73
            File::safeCloseFile($file_resource);
74
        }
75
    }
76
77
78
79
80
    /**
81
     * @param $pid_file_existed
82
     * @param $file_resource
83
     * @throws \Exception
84
     */
85
    private static function safeSetPidIntoFile($pid_file_existed, $file_resource)
86
    {
87
        if ($pid_file_existed) {
88
            self::pidNotActual($file_resource);
89
        }
90
91
        $self_pid = getmypid();
92
        self::setPidIntoFile($self_pid, $file_resource);
93
94
        if ($pid_file_existed) {
95
            self::pidFileUpdated($self_pid);
96
        }
97
    }
98
99
    /**
100
     * @param $file_resource
101
     * @throws \Exception
102
     * @throws \DanchukAS\DenyMultiplyRun\Exception\ProcessExisted
103
     */
104
    private static function pidNotActual($file_resource)
105
    {
106
        self::$prevPid = null;
107
108
        try {
109
            self::$prevPid = self::getPidFromFile($file_resource);
110
            self::pidNoExisting(self::$prevPid);
111
        } catch (PidFileEmpty $exception) {
112
            //@todo when add Debug mode fix
113
//            // if file was once empty is not critical.
114
//            // It was after crash daemon.
115
//            // There are signal for admin/developer.
116
//            trigger_error((string)$exception);
117
        }
118
        File::truncateFile($file_resource);
119
    }
120
121
    /**
122
     * @param resource $pidFileResource Дескриптор файла доступного для читання в якому знаходиться PID.
123
     * @return int PID з файла
124
     * @throws \Exception
125
     */
126
    private static function getPidFromFile($pidFileResource)
127
    {
128
        // Розмір PID (int в ОС) навряд буде більший ніж розмір int в PHP.
129
        // Зазвичай PID має до 5 цифр.
130
        // @todo: if error - warning, error_handler, ...
131
        $pid_from_file = fread($pidFileResource, 64);
132
133
134
        if (false === $pid_from_file) {
135
            throw new ReadFileFail('pid-файл є, але прочитати що в ньому не вдалось.');
136
        }
137
138
        return self::validatePid($pid_from_file);
139
    }
140
141
    /**
142
     * @param string $pid_from_file
143
     * @return int
144
     * @throws \DanchukAS\DenyMultiplyRun\Exception\ConvertPidFail
145
     * @throws \DanchukAS\DenyMultiplyRun\Exception\PidLessMin
146
     * @throws PidBiggerMax
147
     * @throws PidFileEmpty
148
     */
149
    private static function validatePid(string $pid_from_file): int
150
    {
151
        // На випадок коли станеться виліт скрипта після створення файла і до запису ІД.
152
        self::pidIsNoEmpty($pid_from_file);
153
154
        $pid_int = (int) $pid_from_file;
155
        //@todo when add Debug mode fix check
156
        // verify converting. (PHP_MAX_INT)
157
        // verify PID in file is right (something else instead ciphers).
158
        if ("{$pid_int}" !== $pid_from_file) {
159
            $message = "pid_int({$pid_int}) !== pid_string($pid_from_file)"
160
                . ", or pid_string($pid_from_file) is not Process ID)";
161
            throw new ConvertPidFail($message);
162
        }
163
164
        self::pidIsPossible($pid_int);
165
166
        return $pid_int;
167
    }
168
169
    /**
170
     * @param string $pid
171
     * @throws PidFileEmpty
172
     */
173
    private static function pidIsNoEmpty(string $pid)
174
    {
175
        if ('' === $pid) {
176
            throw new PidFileEmpty();
177
        }
178
    }
179
180
    /**
181
     * Verify possible value of PID in file: less than max possible on OS.
182
     * @param int $pid_int
183
     * @throws PidBiggerMax
184
     * @throws PidLessMin
185
     */
186
    private static function pidIsPossible($pid_int)
187
    {
188
        //@todo when add Debug mode fix check
189
        if ($pid_int < 0) {
190
            $message = "PID in file has unavailable value: $pid_int. PID must be no negative.";
191
            throw new PidLessMin($message);
192
        }
193
194
        // if PID not available - why it happens ?
195
        // For *nix system
196
        $pid_max_storage = '/proc/sys/kernel/pid_max';
197
        if (file_exists($pid_max_storage)) {
198
            $pid_max = (int)file_get_contents($pid_max_storage);
199
            if ($pid_max < $pid_int) {
200
                $message = "PID in file has unavailable value: $pid_int. In /proc/sys/kernel/pid_max set $pid_max.";
201
                throw new PidBiggerMax($message);
202
            }
203
        }
204
    }
205
206
    /**
207
     * @param int $pid
208
     *
209
     * @throws ProcessExisted
210
     */
211
    private static function pidNoExisting($pid)
212
    {
213
        if (// Посилає сигнал процесу щоб дізнатись чи він існує.
214
            // Якщо true - точно існує.
215
            // якщо false - процес може і бути, але запущений під іншим користувачем або інші ситуації.
216
            true === posix_kill($pid, 0)
217
            // 3 = No such process (тестувалось в Ubuntu 14, FreeBSD 9. В інших ОС можуть відрізнятись)
218
            // Якщо процеса точно в даний момент нема такого.
219
            // Для визова цієї функції необхідний попередній визов posix_kill.
220
            || posix_get_last_error() !== 3
221
        ) {
222
            throw new ProcessExisted($pid);
223
        }
224
    }
225
226
227
228
    /**
229
     * @param int $self_pid
230
     * @param $pidFileResource
231
     *
232
     * @throws \Exception
233
     */
234
    private static function setPidIntoFile($self_pid, $pidFileResource)
235
    {
236
        $self_pid_str = (string)$self_pid;
237
        $pid_length = strlen($self_pid_str);
238
        $write_length = fwrite($pidFileResource, $self_pid_str, $pid_length);
239
        if ($write_length !== $pid_length) {
240
            $message = "не вдось записати pid в pid-файл. Записано $write_length байт замість $pid_length";
241
            throw new \RuntimeException($message);
242
        }
243
    }
244
245
    /**
246
     * @param $self_pid
247
     */
248
    private static function pidFileUpdated($self_pid)
0 ignored issues
show
Unused Code introduced by
The parameter $self_pid is not used and could be removed. ( Ignorable by Annotation )

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

248
    private static function pidFileUpdated(/** @scrutinizer ignore-unused */ $self_pid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
249
    {
250
        // @todo: maybe reference with Debug mode.
251
//        $message_reason = null === self::$prevPid
252
//            ? ', but file empty.'
253
//            : ', but process with contained ID(' . self::$prevPid . ') in it is not exist.';
254
//        $message = 'pid-file exist' . $message_reason
255
//            . ' pid-file updated with pid this process: ' . $self_pid;
256
//
257
//        trigger_error($message);
258
    }
259
260
261
262
    /**
263
     * Відключає встановлену заборону паралельного запуска у яких спільний $pidFilePath
264
     * @todo добавити перевірку що цей файл ще для цього процеса,
265
     * може цей файл вже був видалений вручну, і створений іншим процесом.
266
     *
267
     * @param string $pidFilePath
268
     *
269
     * @throws DeleteFileFail
270
     */
271
    public static function deletePidFile($pidFilePath)
272
    {
273
        File::deleteFile($pidFilePath);
274
    }
275
276
277
}
278