Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
37.28% |
390 / 1046 |
|
17.39% |
12 / 69 |
CRAP | |
0.00% |
0 / 1 |
Worker | |
37.28% |
390 / 1046 |
|
17.39% |
12 / 69 |
46680.93 | |
0.00% |
0 / 1 |
__construct | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
2.00 | |||
runAll | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
2.05 | |||
checkSapiEnv | |
88.10% |
37 / 42 |
|
0.00% |
0 / 1 |
6.06 | |||
initStdOut | |
57.14% |
4 / 7 |
|
0.00% |
0 / 1 |
6.97 | |||
hasColorSupport | |
40.00% |
4 / 10 |
|
0.00% |
0 / 1 |
21.82 | |||
init | |
78.12% |
25 / 32 |
|
0.00% |
0 / 1 |
13.51 | |||
initGlobalEvent | |
50.00% |
6 / 12 |
|
0.00% |
0 / 1 |
10.50 | |||
lock | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
6.02 | |||
initWorkers | |
80.95% |
17 / 21 |
|
0.00% |
0 / 1 |
13.00 | |||
getAllWorkers | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getEventLoop | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMainSocket | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initId | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getCurrentUser | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
displayUI | |
78.38% |
29 / 37 |
|
0.00% |
0 / 1 |
15.98 | |||
getVersionLine | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getUiColumns | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
getSingleLineTotalLength | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
parseCommand | |
34.04% |
32 / 94 |
|
0.00% |
0 / 1 |
429.82 | |||
getArgv | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
formatProcessStatusData | |
0.00% |
0 / 65 |
|
0.00% |
0 / 1 |
182 | |||
formatConnectionStatusData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
installSignal | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
reinstallSignal | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
signalHandler | |
12.00% |
3 / 25 |
|
0.00% |
0 / 1 |
110.13 | |||
getSignalName | |
20.00% |
2 / 10 |
|
0.00% |
0 / 1 |
72.95 | |||
daemonize | |
13.33% |
2 / 15 |
|
0.00% |
0 / 1 |
49.66 | |||
resetStd | |
11.11% |
2 / 18 |
|
0.00% |
0 / 1 |
65.89 | |||
saveMasterPid | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
3.58 | |||
getAllWorkerPids | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
forkWorkers | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
forkWorkersForLinux | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
5.12 | |||
forkWorkersForWindows | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
90 | |||
getStartFilesForWindows | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
forkOneWorkerForWindows | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
checkWorkerStatusForWindows | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
forkOneWorkerForLinux | |
71.79% |
28 / 39 |
|
0.00% |
0 / 1 |
10.82 | |||
getId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setUserAndGroup | |
40.00% |
6 / 15 |
|
0.00% |
0 / 1 |
26.50 | |||
setProcessTitle | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
monitorWorkers | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
monitorWorkersForLinux | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
210 | |||
monitorWorkersForWindows | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
exitAndClearAll | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
reload | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
210 | |||
stopAll | |
15.62% |
5 / 32 |
|
0.00% |
0 / 1 |
285.90 | |||
checkIfChildRunning | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
20 | |||
getStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGracefulStop | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
writeStatisticsToStatusFile | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
132 | |||
getUiColumnLength | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
writeConnectionsStatisticsToStatusFile | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
272 | |||
checkErrors | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
90 | |||
getErrorType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
log | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
safeEcho | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
4.25 | |||
listen | |
71.88% |
23 / 32 |
|
0.00% |
0 / 1 |
20.01 | |||
unlisten | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
checkPortAvailable | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
10.02 | |||
parseSocketAddress | |
66.67% |
10 / 15 |
|
0.00% |
0 / 1 |
10.37 | |||
pauseAccept | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
8.74 | |||
resumeAccept | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
5.20 | |||
getSocketName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
run | |
23.08% |
3 / 13 |
|
0.00% |
0 / 1 |
29.30 | |||
stop | |
23.08% |
3 / 13 |
|
0.00% |
0 / 1 |
37.13 | |||
acceptTcpConnection | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
4.02 | |||
acceptUdpConnection | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
156 | |||
checkMasterIsAlive | |
15.38% |
2 / 13 |
|
0.00% |
0 / 1 |
36.69 | |||
isRunning | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This file is part of workerman. |
4 | * |
5 | * Licensed under The MIT License |
6 | * For full copyright and license information, please see the MIT-LICENSE.txt |
7 | * Redistributions of files must retain the above copyright notice. |
8 | * |
9 | * @author walkor<walkor@workerman.net> |
10 | * @copyright walkor<walkor@workerman.net> |
11 | * @link http://www.workerman.net/ |
12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License |
13 | */ |
14 | |
15 | declare(strict_types=1); |
16 | |
17 | namespace Workerman; |
18 | |
19 | use AllowDynamicProperties; |
20 | use Exception; |
21 | use RuntimeException; |
22 | use stdClass; |
23 | use Stringable; |
24 | use Throwable; |
25 | use Workerman\Connection\ConnectionInterface; |
26 | use Workerman\Connection\TcpConnection; |
27 | use Workerman\Connection\UdpConnection; |
28 | use Workerman\Coroutine; |
29 | use Workerman\Events\Event; |
30 | use Workerman\Events\EventInterface; |
31 | use Workerman\Events\Fiber; |
32 | use Workerman\Events\Select; |
33 | use Workerman\Events\Swoole; |
34 | use Workerman\Events\Swow; |
35 | use function defined; |
36 | use function function_exists; |
37 | use function is_resource; |
38 | use function method_exists; |
39 | use function restore_error_handler; |
40 | use function set_error_handler; |
41 | use function stream_socket_accept; |
42 | use function stream_socket_recvfrom; |
43 | use function substr; |
44 | use function array_walk; |
45 | use function get_class; |
46 | use const DIRECTORY_SEPARATOR; |
47 | use const PHP_SAPI; |
48 | use const PHP_VERSION; |
49 | use const STDOUT; |
50 | |
51 | /** |
52 | * Worker class |
53 | * A container for listening ports |
54 | */ |
55 | #[AllowDynamicProperties] |
56 | class Worker |
57 | { |
58 | /** |
59 | * Version. |
60 | * |
61 | * @var string |
62 | */ |
63 | final public const VERSION = '5.1.2'; |
64 | |
65 | /** |
66 | * Status initial. |
67 | * |
68 | * @var int |
69 | */ |
70 | public const STATUS_INITIAL = 0; |
71 | |
72 | /** |
73 | * Status starting. |
74 | * |
75 | * @var int |
76 | */ |
77 | public const STATUS_STARTING = 1; |
78 | |
79 | /** |
80 | * Status running. |
81 | * |
82 | * @var int |
83 | */ |
84 | public const STATUS_RUNNING = 2; |
85 | |
86 | /** |
87 | * Status shutdown. |
88 | * |
89 | * @var int |
90 | */ |
91 | public const STATUS_SHUTDOWN = 4; |
92 | |
93 | /** |
94 | * Status reloading. |
95 | * |
96 | * @var int |
97 | */ |
98 | public const STATUS_RELOADING = 8; |
99 | |
100 | /** |
101 | * Default backlog. Backlog is the maximum length of the queue of pending connections. |
102 | * |
103 | * @var int |
104 | */ |
105 | public const DEFAULT_BACKLOG = 102400; |
106 | |
107 | /** |
108 | * The safe distance for columns adjacent |
109 | * |
110 | * @var int |
111 | */ |
112 | public const UI_SAFE_LENGTH = 4; |
113 | |
114 | /** |
115 | * Worker id. |
116 | * |
117 | * @var int |
118 | */ |
119 | public int $id = 0; |
120 | |
121 | /** |
122 | * Name of the worker processes. |
123 | * |
124 | * @var string |
125 | */ |
126 | public string $name = 'none'; |
127 | |
128 | /** |
129 | * Number of worker processes. |
130 | * |
131 | * @var int |
132 | */ |
133 | public int $count = 1; |
134 | |
135 | /** |
136 | * Unix user of processes, needs appropriate privileges (usually root). |
137 | * |
138 | * @var string |
139 | */ |
140 | public string $user = ''; |
141 | |
142 | /** |
143 | * Unix group of processes, needs appropriate privileges (usually root). |
144 | * |
145 | * @var string |
146 | */ |
147 | public string $group = ''; |
148 | |
149 | /** |
150 | * reloadable. |
151 | * |
152 | * @var bool |
153 | */ |
154 | public bool $reloadable = true; |
155 | |
156 | /** |
157 | * reuse port. |
158 | * |
159 | * @var bool |
160 | */ |
161 | public bool $reusePort = false; |
162 | |
163 | /** |
164 | * Emitted when worker processes is starting. |
165 | * |
166 | * @var ?callable |
167 | */ |
168 | public $onWorkerStart = null; |
169 | |
170 | /** |
171 | * Emitted when a socket connection is successfully established. |
172 | * |
173 | * @var ?callable |
174 | */ |
175 | public $onConnect = null; |
176 | |
177 | /** |
178 | * Emitted before websocket handshake (Only works when protocol is ws). |
179 | * |
180 | * @var ?callable |
181 | */ |
182 | public $onWebSocketConnect = null; |
183 | |
184 | /** |
185 | * Emitted after websocket handshake (Only works when protocol is ws). |
186 | * |
187 | * @var ?callable |
188 | */ |
189 | public $onWebSocketConnected = null; |
190 | |
191 | /** |
192 | * Emitted when data is received. |
193 | * |
194 | * @var ?callable |
195 | */ |
196 | public $onMessage = null; |
197 | |
198 | /** |
199 | * Emitted when the other end of the socket sends a FIN packet. |
200 | * |
201 | * @var ?callable |
202 | */ |
203 | public $onClose = null; |
204 | |
205 | /** |
206 | * Emitted when an error occurs with connection. |
207 | * |
208 | * @var ?callable |
209 | */ |
210 | public $onError = null; |
211 | |
212 | /** |
213 | * Emitted when the send buffer becomes full. |
214 | * |
215 | * @var ?callable |
216 | */ |
217 | public $onBufferFull = null; |
218 | |
219 | /** |
220 | * Emitted when the send buffer becomes empty. |
221 | * |
222 | * @var ?callable |
223 | */ |
224 | public $onBufferDrain = null; |
225 | |
226 | /** |
227 | * Emitted when worker processes has stopped. |
228 | * |
229 | * @var ?callable |
230 | */ |
231 | public $onWorkerStop = null; |
232 | |
233 | /** |
234 | * Emitted when worker processes receives reload signal. |
235 | * |
236 | * @var ?callable |
237 | */ |
238 | public $onWorkerReload = null; |
239 | |
240 | /** |
241 | * Transport layer protocol. |
242 | * |
243 | * @var string |
244 | */ |
245 | public string $transport = 'tcp'; |
246 | |
247 | /** |
248 | * Store all connections of clients. |
249 | * |
250 | * @var TcpConnection[] |
251 | */ |
252 | public array $connections = []; |
253 | |
254 | /** |
255 | * Application layer protocol. |
256 | * |
257 | * @var ?string |
258 | */ |
259 | public ?string $protocol = null; |
260 | |
261 | /** |
262 | * Pause accept new connections or not. |
263 | * |
264 | * @var bool |
265 | */ |
266 | protected bool $pauseAccept = true; |
267 | |
268 | /** |
269 | * Is worker stopping ? |
270 | * |
271 | * @var bool |
272 | */ |
273 | public bool $stopping = false; |
274 | |
275 | /** |
276 | * EventLoop class. |
277 | * |
278 | * @var ?string |
279 | */ |
280 | public ?string $eventLoop = null; |
281 | |
282 | /** |
283 | * Daemonize. |
284 | * |
285 | * @var bool |
286 | */ |
287 | public static bool $daemonize = false; |
288 | |
289 | /** |
290 | * Standard output stream |
291 | * |
292 | * @var resource |
293 | */ |
294 | public static $outputStream; |
295 | |
296 | /** |
297 | * Stdout file. |
298 | * |
299 | * @var string |
300 | */ |
301 | public static string $stdoutFile = '/dev/null'; |
302 | |
303 | /** |
304 | * The file to store master process PID. |
305 | * |
306 | * @var string |
307 | */ |
308 | public static string $pidFile = ''; |
309 | |
310 | /** |
311 | * The file used to store the master process status. |
312 | * |
313 | * @var string |
314 | */ |
315 | public static string $statusFile = ''; |
316 | |
317 | /** |
318 | * Log file. |
319 | * |
320 | * @var string |
321 | */ |
322 | public static string $logFile = ''; |
323 | |
324 | /** |
325 | * Global event loop. |
326 | * |
327 | * @var ?EventInterface |
328 | */ |
329 | public static ?EventInterface $globalEvent = null; |
330 | |
331 | /** |
332 | * Emitted when the master process get reload signal. |
333 | * |
334 | * @var ?callable |
335 | */ |
336 | public static $onMasterReload = null; |
337 | |
338 | /** |
339 | * Emitted when the master process terminated. |
340 | * |
341 | * @var ?callable |
342 | */ |
343 | public static $onMasterStop = null; |
344 | |
345 | /** |
346 | * Emitted when worker processes exited. |
347 | * |
348 | * @var ?callable |
349 | */ |
350 | public static $onWorkerExit = null; |
351 | |
352 | /** |
353 | * EventLoopClass |
354 | * |
355 | * @var ?class-string<EventInterface> |
356 | */ |
357 | public static ?string $eventLoopClass = null; |
358 | |
359 | /** |
360 | * After sending the stop command to the child process stopTimeout seconds, |
361 | * if the process is still living then forced to kill. |
362 | * |
363 | * @var int |
364 | */ |
365 | public static int $stopTimeout = 2; |
366 | |
367 | /** |
368 | * Command |
369 | * |
370 | * @var string |
371 | */ |
372 | public static string $command = ''; |
373 | |
374 | /** |
375 | * The PID of master process. |
376 | * |
377 | * @var int |
378 | */ |
379 | protected static int $masterPid = 0; |
380 | |
381 | /** |
382 | * Listening socket. |
383 | * |
384 | * @var ?resource |
385 | */ |
386 | protected $mainSocket = null; |
387 | |
388 | /** |
389 | * Socket name. The format is like this http://0.0.0.0:80 . |
390 | * |
391 | * @var string |
392 | */ |
393 | protected string $socketName = ''; |
394 | |
395 | /** |
396 | * Context of socket. |
397 | * |
398 | * @var resource |
399 | */ |
400 | protected $socketContext = null; |
401 | |
402 | /** |
403 | * @var stdClass |
404 | */ |
405 | protected stdClass $context; |
406 | |
407 | /** |
408 | * All worker instances. |
409 | * |
410 | * @var Worker[] |
411 | */ |
412 | protected static array $workers = []; |
413 | |
414 | /** |
415 | * All worker processes pid. |
416 | * The format is like this [worker_id=>[pid=>pid, pid=>pid, ..], ..] |
417 | * |
418 | * @var array |
419 | */ |
420 | protected static array $pidMap = []; |
421 | |
422 | /** |
423 | * All worker processes waiting for restart. |
424 | * The format is like this [pid=>pid, pid=>pid]. |
425 | * |
426 | * @var array |
427 | */ |
428 | protected static array $pidsToRestart = []; |
429 | |
430 | /** |
431 | * Mapping from PID to worker process ID. |
432 | * The format is like this [worker_id=>[0=>$pid, 1=>$pid, ..], ..]. |
433 | * |
434 | * @var array |
435 | */ |
436 | protected static array $idMap = []; |
437 | |
438 | /** |
439 | * Current status. |
440 | * |
441 | * @var int |
442 | */ |
443 | protected static int $status = self::STATUS_INITIAL; |
444 | |
445 | /** |
446 | * UI data. |
447 | * |
448 | * @var array|int[] |
449 | */ |
450 | protected static array $uiLengthData = []; |
451 | |
452 | /** |
453 | * The file to store status info of current worker process. |
454 | * |
455 | * @var string |
456 | */ |
457 | protected static string $statisticsFile = ''; |
458 | |
459 | /** |
460 | * The file to store status info of connections. |
461 | * |
462 | * @var string |
463 | */ |
464 | protected static string $connectionsFile = ''; |
465 | |
466 | /** |
467 | * Start file. |
468 | * |
469 | * @var string |
470 | */ |
471 | protected static string $startFile = ''; |
472 | |
473 | /** |
474 | * Processes for windows. |
475 | * |
476 | * @var array |
477 | */ |
478 | protected static array $processForWindows = []; |
479 | |
480 | /** |
481 | * Status info of current worker process. |
482 | * |
483 | * @var array |
484 | */ |
485 | protected static array $globalStatistics = [ |
486 | 'start_timestamp' => 0, |
487 | 'worker_exit_info' => [] |
488 | ]; |
489 | |
490 | /** |
491 | * PHP built-in protocols. |
492 | * |
493 | * @var array<string, string> |
494 | */ |
495 | public const BUILD_IN_TRANSPORTS = [ |
496 | 'tcp' => 'tcp', |
497 | 'udp' => 'udp', |
498 | 'unix' => 'unix', |
499 | 'ssl' => 'tcp' |
500 | ]; |
501 | |
502 | /** |
503 | * PHP built-in error types. |
504 | * |
505 | * @var array<int, string> |
506 | */ |
507 | public const ERROR_TYPE = [ |
508 | E_ERROR => 'E_ERROR', |
509 | E_WARNING => 'E_WARNING', |
510 | E_PARSE => 'E_PARSE', |
511 | E_NOTICE => 'E_NOTICE', |
512 | E_CORE_ERROR => 'E_CORE_ERROR', |
513 | E_CORE_WARNING => 'E_CORE_WARNING', |
514 | E_COMPILE_ERROR => 'E_COMPILE_ERROR', |
515 | E_COMPILE_WARNING => 'E_COMPILE_WARNING', |
516 | E_USER_ERROR => 'E_USER_ERROR', |
517 | E_USER_WARNING => 'E_USER_WARNING', |
518 | E_USER_NOTICE => 'E_USER_NOTICE', |
519 | E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', |
520 | E_DEPRECATED => 'E_DEPRECATED', |
521 | E_USER_DEPRECATED => 'E_USER_DEPRECATED' |
522 | ]; |
523 | |
524 | /** |
525 | * Graceful stop or not. |
526 | * |
527 | * @var bool |
528 | */ |
529 | protected static bool $gracefulStop = false; |
530 | |
531 | /** |
532 | * If $outputStream support decorated |
533 | * |
534 | * @var bool |
535 | */ |
536 | protected static bool $outputDecorated; |
537 | |
538 | /** |
539 | * Worker object's hash id(unique identifier). |
540 | * |
541 | * @var ?string |
542 | */ |
543 | protected ?string $workerId = null; |
544 | |
545 | /** |
546 | * Constructor. |
547 | * |
548 | * @param string|null $socketName |
549 | * @param array $socketContext |
550 | */ |
551 | public function __construct(?string $socketName = null, array $socketContext = []) |
552 | { |
553 | // Save all worker instances. |
554 | $this->workerId = spl_object_hash($this); |
555 | $this->context = new stdClass(); |
556 | static::$workers[$this->workerId] = $this; |
557 | static::$pidMap[$this->workerId] = []; |
558 | |
559 | // Context for socket. |
560 | if ($socketName) { |
561 | $this->socketName = $socketName; |
562 | $socketContext['socket']['backlog'] ??= static::DEFAULT_BACKLOG; |
563 | $this->socketContext = stream_context_create($socketContext); |
564 | } |
565 | |
566 | // Set an empty onMessage callback. |
567 | $this->onMessage = function () { |
568 | // Empty. |
569 | }; |
570 | |
571 | } |
572 | |
573 | /** |
574 | * Run all worker instances. |
575 | * |
576 | * @return void |
577 | */ |
578 | public static function runAll(): void |
579 | { |
580 | try { |
581 | static::checkSapiEnv(); |
582 | static::initStdOut(); |
583 | static::init(); |
584 | static::parseCommand(); |
585 | static::checkPortAvailable(); |
586 | static::lock(); |
587 | static::daemonize(); |
588 | static::initWorkers(); |
589 | static::installSignal(); |
590 | static::saveMasterPid(); |
591 | static::lock(LOCK_UN); |
592 | static::displayUI(); |
593 | static::forkWorkers(); |
594 | static::resetStd(); |
595 | static::monitorWorkers(); |
596 | } catch (Throwable $e) { |
597 | static::log($e); |
598 | } |
599 | } |
600 | |
601 | /** |
602 | * Check sapi. |
603 | * |
604 | * @return void |
605 | */ |
606 | protected static function checkSapiEnv(): void |
607 | { |
608 | // Only for cli and micro. |
609 | if (!in_array(PHP_SAPI, ['cli', 'micro'])) { |
610 | exit("Only run in command line mode" . PHP_EOL); |
611 | } |
612 | // Check pcntl and posix extension for unix. |
613 | if (DIRECTORY_SEPARATOR === '/') { |
614 | foreach (['pcntl', 'posix'] as $name) { |
615 | if (!extension_loaded($name)) { |
616 | exit("Please install $name extension" . PHP_EOL); |
617 | } |
618 | } |
619 | } |
620 | // Check disable functions. |
621 | $disabledFunctions = explode(',', ini_get('disable_functions')); |
622 | $disabledFunctions = array_map('trim', $disabledFunctions); |
623 | $functionsToCheck = [ |
624 | 'stream_socket_server', |
625 | 'stream_socket_accept', |
626 | 'stream_socket_client', |
627 | 'pcntl_signal_dispatch', |
628 | 'pcntl_signal', |
629 | 'pcntl_alarm', |
630 | 'pcntl_fork', |
631 | 'pcntl_wait', |
632 | 'posix_getuid', |
633 | 'posix_getpwuid', |
634 | 'posix_kill', |
635 | 'posix_setsid', |
636 | 'posix_getpid', |
637 | 'posix_getpwnam', |
638 | 'posix_getgrnam', |
639 | 'posix_getgid', |
640 | 'posix_setgid', |
641 | 'posix_initgroups', |
642 | 'posix_setuid', |
643 | 'posix_isatty', |
644 | 'proc_open', |
645 | 'proc_get_status', |
646 | 'proc_close', |
647 | 'shell_exec', |
648 | 'exec', |
649 | 'putenv', |
650 | 'getenv', |
651 | ]; |
652 | $disabled = array_intersect($functionsToCheck, $disabledFunctions); |
653 | if (!empty($disabled)) { |
654 | $iniFilePath = (string)php_ini_loaded_file(); |
655 | exit('Notice: '. implode(',', $disabled) . " are disabled by disable_functions. " . PHP_EOL |
656 | . "Please remove them from disable_functions in $iniFilePath" . PHP_EOL); |
657 | } |
658 | } |
659 | |
660 | /** |
661 | * Init stdout. |
662 | * |
663 | * @return void |
664 | */ |
665 | protected static function initStdOut(): void |
666 | { |
667 | $defaultStream = fn () => defined('STDOUT') ? STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w')); |
668 | static::$outputStream ??= $defaultStream(); //@phpstan-ignore-line |
669 | if (!is_resource(self::$outputStream) || get_resource_type(self::$outputStream) !== 'stream') { |
670 | $type = get_debug_type(self::$outputStream); |
671 | static::$outputStream = $defaultStream(); |
672 | throw new RuntimeException(sprintf('The $outputStream must to be a stream, %s given', $type)); |
673 | } |
674 | |
675 | static::$outputDecorated ??= self::hasColorSupport(); |
676 | } |
677 | |
678 | /** |
679 | * Borrowed from the symfony console |
680 | * @link https://github.com/symfony/console/blob/0d14a9f6d04d4ac38a8cea1171f4554e325dae92/Output/StreamOutput.php#L92 |
681 | */ |
682 | private static function hasColorSupport(): bool |
683 | { |
684 | // Follow https://no-color.org/ |
685 | if (getenv('NO_COLOR') !== false) { |
686 | return false; |
687 | } |
688 | |
689 | if (getenv('TERM_PROGRAM') === 'Hyper') { |
690 | return true; |
691 | } |
692 | |
693 | if (DIRECTORY_SEPARATOR === '\\') { |
694 | return (function_exists('sapi_windows_vt100_support') && @sapi_windows_vt100_support(self::$outputStream)) |
695 | || getenv('ANSICON') !== false |
696 | || getenv('ConEmuANSI') === 'ON' |
697 | || getenv('TERM') === 'xterm'; |
698 | } |
699 | |
700 | return stream_isatty(self::$outputStream); |
701 | } |
702 | |
703 | /** |
704 | * Init. |
705 | * |
706 | * @return void |
707 | */ |
708 | protected static function init(): void |
709 | { |
710 | set_error_handler(static function (int $code, string $msg, string $file, int $line): bool { |
711 | static::safeEcho(sprintf("%s \"%s\" in file %s on line %d\n", static::getErrorType($code), $msg, $file, $line)); |
712 | return true; |
713 | }); |
714 | |
715 | // $_SERVER. |
716 | $_SERVER['SERVER_SOFTWARE'] = 'Workerman/' . static::VERSION; |
717 | $_SERVER['SERVER_START_TIME'] = time(); |
718 | |
719 | // Start file. |
720 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
721 | static::$startFile = static::$startFile ?: end($backtrace)['file']; |
722 | $startFilePrefix = basename(static::$startFile); |
723 | $startFileDir = dirname(static::$startFile); |
724 | |
725 | // Compatible with older workerman versions for pid file. |
726 | if (empty(static::$pidFile)) { |
727 | $unique_prefix = \str_replace('/', '_', static::$startFile); |
728 | $file = __DIR__ . "/../../$unique_prefix.pid"; |
729 | if (is_file($file)) { |
730 | static::$pidFile = $file; |
731 | } |
732 | } |
733 | |
734 | // Pid file. |
735 | static::$pidFile = static::$pidFile ?: sprintf('%s/workerman.%s.pid', $startFileDir, $startFilePrefix); |
736 | |
737 | // Status file. |
738 | static::$statusFile = static::$statusFile ?: sprintf('%s/workerman.%s.status', $startFileDir, $startFilePrefix); |
739 | static::$statisticsFile = static::$statisticsFile ?: static::$statusFile; |
740 | static::$connectionsFile = static::$connectionsFile ?: static::$statusFile . '.connection'; |
741 | |
742 | // Log file. |
743 | static::$logFile = static::$logFile ?: sprintf('%s/workerman.log', $startFileDir); |
744 | |
745 | if (static::$logFile !== '/dev/null' && !is_file(static::$logFile)) { |
746 | // if /runtime/logs default folder not exists |
747 | if (!is_dir(dirname(static::$logFile))) { |
748 | mkdir(dirname(static::$logFile), 0777, true); |
749 | } |
750 | touch(static::$logFile); |
751 | chmod(static::$logFile, 0644); |
752 | } |
753 | |
754 | // State. |
755 | static::$status = static::STATUS_STARTING; |
756 | |
757 | // Init global event. |
758 | static::initGlobalEvent(); |
759 | |
760 | // For statistics. |
761 | static::$globalStatistics['start_timestamp'] = time(); |
762 | |
763 | // Process title. |
764 | static::setProcessTitle('WorkerMan: master process start_file=' . static::$startFile); |
765 | |
766 | // Init data for worker id. |
767 | static::initId(); |
768 | |
769 | // Timer init. |
770 | Timer::init(); |
771 | |
772 | restore_error_handler(); |
773 | } |
774 | |
775 | /** |
776 | * Init global event. |
777 | * |
778 | * @return void |
779 | */ |
780 | protected static function initGlobalEvent(): void |
781 | { |
782 | if (static::$globalEvent !== null) { |
783 | static::$eventLoopClass = get_class(static::$globalEvent); |
784 | static::$globalEvent = null; |
785 | return; |
786 | } |
787 | |
788 | if (!empty(static::$eventLoopClass)) { |
789 | if (!is_subclass_of(static::$eventLoopClass, EventInterface::class)) { |
790 | throw new RuntimeException(sprintf('%s::$eventLoopClass must implement %s', static::class, EventInterface::class)); |
791 | } |
792 | return; |
793 | } |
794 | |
795 | static::$eventLoopClass = match (true) { |
796 | extension_loaded('event') => Event::class, |
797 | default => Select::class, |
798 | }; |
799 | } |
800 | |
801 | /** |
802 | * Lock. |
803 | * |
804 | * @param int $flag |
805 | * @return void |
806 | */ |
807 | protected static function lock(int $flag = LOCK_EX): void |
808 | { |
809 | static $fd; |
810 | if (DIRECTORY_SEPARATOR !== '/') { |
811 | return; |
812 | } |
813 | $lockFile = static::$pidFile . '.lock'; |
814 | $fd = $fd ?: fopen($lockFile, 'a+'); |
815 | if ($fd) { |
816 | flock($fd, $flag); |
817 | if ($flag === LOCK_UN) { |
818 | fclose($fd); |
819 | $fd = null; |
820 | clearstatcache(); |
821 | if (is_file($lockFile)) { |
822 | unlink($lockFile); |
823 | } |
824 | } |
825 | } |
826 | } |
827 | |
828 | /** |
829 | * Init All worker instances. |
830 | * |
831 | * @return void |
832 | */ |
833 | protected static function initWorkers(): void |
834 | { |
835 | if (DIRECTORY_SEPARATOR !== '/') { |
836 | return; |
837 | } |
838 | |
839 | foreach (static::$workers as $worker) { |
840 | // Worker name. |
841 | if (empty($worker->name)) { |
842 | $worker->name = 'none'; |
843 | } |
844 | |
845 | // Get unix user of the worker process. |
846 | if (empty($worker->user)) { |
847 | $worker->user = static::getCurrentUser(); |
848 | } else { |
849 | if (posix_getuid() !== 0 && $worker->user !== static::getCurrentUser()) { |
850 | static::log('Warning: You must have the root privileges to change uid and gid.'); |
851 | } |
852 | } |
853 | |
854 | // Socket name. |
855 | $worker->context->statusSocket = $worker->getSocketName(); |
856 | |
857 | // Event-loop name. |
858 | $eventLoopName = $worker->eventLoop ?: static::$eventLoopClass; |
859 | $worker->context->eventLoopName = strtolower(substr($eventLoopName, strrpos($eventLoopName, '\\') + 1)); |
860 | |
861 | // Status name. |
862 | $worker->context->statusState = '<g> [OK] </g>'; |
863 | |
864 | // Get column mapping for UI |
865 | foreach (static::getUiColumns() as $columnName => $prop) { |
866 | !isset($worker->$prop) && !isset($worker->context->$prop) && $worker->context->$prop = 'NNNN'; |
867 | $propLength = strlen((string)($worker->$prop ?? $worker->context->$prop)); |
868 | $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; |
869 | static::$uiLengthData[$key] = max(static::$uiLengthData[$key] ?? 2 * static::UI_SAFE_LENGTH, $propLength); |
870 | } |
871 | |
872 | // Listen. |
873 | if (!$worker->reusePort) { |
874 | $worker->listen(); |
875 | $worker->pauseAccept(); |
876 | } |
877 | } |
878 | } |
879 | |
880 | /** |
881 | * Get all worker instances. |
882 | * |
883 | * @return Worker[] |
884 | */ |
885 | public static function getAllWorkers(): array |
886 | { |
887 | return static::$workers; |
888 | } |
889 | |
890 | /** |
891 | * Get global event-loop instance. |
892 | * |
893 | * @return EventInterface |
894 | */ |
895 | public static function getEventLoop(): EventInterface |
896 | { |
897 | return static::$globalEvent; |
898 | } |
899 | |
900 | /** |
901 | * Get main socket resource |
902 | * |
903 | * @return resource |
904 | */ |
905 | public function getMainSocket(): mixed |
906 | { |
907 | return $this->mainSocket; |
908 | } |
909 | |
910 | /** |
911 | * Init idMap. |
912 | * |
913 | * @return void |
914 | */ |
915 | protected static function initId(): void |
916 | { |
917 | foreach (static::$workers as $workerId => $worker) { |
918 | $newIdMap = []; |
919 | $worker->count = max($worker->count, 1); |
920 | for ($key = 0; $key < $worker->count; $key++) { |
921 | $newIdMap[$key] = static::$idMap[$workerId][$key] ?? 0; |
922 | } |
923 | static::$idMap[$workerId] = $newIdMap; |
924 | } |
925 | } |
926 | |
927 | /** |
928 | * Get unix user of current process. |
929 | * |
930 | * @return string |
931 | */ |
932 | protected static function getCurrentUser(): string |
933 | { |
934 | $userInfo = posix_getpwuid(posix_getuid()); |
935 | return $userInfo['name'] ?? 'unknown'; |
936 | } |
937 | |
938 | /** |
939 | * Display staring UI. |
940 | * |
941 | * @return void |
942 | */ |
943 | protected static function displayUI(): void |
944 | { |
945 | $tmpArgv = static::getArgv(); |
946 | if (in_array('-q', $tmpArgv)) { |
947 | return; |
948 | } |
949 | |
950 | |
951 | $lineVersion = static::getVersionLine(); |
952 | // For windows |
953 | if (DIRECTORY_SEPARATOR !== '/') { |
954 | static::safeEcho("---------------------------------------------- WORKERMAN -----------------------------------------------\r\n"); |
955 | static::safeEcho($lineVersion); |
956 | static::safeEcho("----------------------------------------------- WORKERS ------------------------------------------------\r\n"); |
957 | static::safeEcho("worker listen processes status\r\n"); |
958 | return; |
959 | } |
960 | |
961 | // For unix |
962 | !defined('LINE_VERSION_LENGTH') && define('LINE_VERSION_LENGTH', strlen($lineVersion)); |
963 | $totalLength = static::getSingleLineTotalLength(); |
964 | $lineOne = '<n>' . str_pad('<w> WORKERMAN </w>', $totalLength + strlen('<w></w>'), '-', STR_PAD_BOTH) . '</n>' . PHP_EOL; |
965 | $lineTwo = str_pad('<w> WORKERS </w>', $totalLength + strlen('<w></w>'), '-', STR_PAD_BOTH) . PHP_EOL; |
966 | static::safeEcho($lineOne . $lineVersion . $lineTwo); |
967 | |
968 | //Show title |
969 | $title = ''; |
970 | foreach (static::getUiColumns() as $columnName => $prop) { |
971 | $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; |
972 | //just keep compatible with listen name |
973 | $columnName === 'socket' && $columnName = 'listen'; |
974 | $title .= "<w>$columnName</w>" . str_pad('', static::getUiColumnLength($key) + static::UI_SAFE_LENGTH - strlen($columnName)); |
975 | } |
976 | $title && static::safeEcho($title . PHP_EOL); |
977 | |
978 | //Show content |
979 | foreach (static::$workers as $worker) { |
980 | $content = ''; |
981 | foreach (static::getUiColumns() as $columnName => $prop) { |
982 | $propValue = (string)($worker->$prop ?? $worker->context->$prop); |
983 | $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; |
984 | preg_match_all("/(<n>|<\/n>|<w>|<\/w>|<g>|<\/g>)/i", $propValue, $matches); |
985 | $placeHolderLength = !empty($matches[0]) ? strlen(implode('', $matches[0])) : 0; |
986 | $content .= str_pad($propValue, static::getUiColumnLength($key) + static::UI_SAFE_LENGTH + $placeHolderLength); |
987 | } |
988 | $content && static::safeEcho($content . PHP_EOL); |
989 | } |
990 | |
991 | //Show last line |
992 | $lineLast = str_pad('', static::getSingleLineTotalLength(), '-') . PHP_EOL; |
993 | !empty($content) && static::safeEcho($lineLast); |
994 | |
995 | if (static::$daemonize) { |
996 | static::safeEcho('Input "php ' . basename(static::$startFile) . ' stop" to stop. Start success.' . "\n\n"); |
997 | } else if (!empty(static::$command)) { |
998 | static::safeEcho("Start success.\n"); // Workerman used as library |
999 | } else { |
1000 | static::safeEcho("Press Ctrl+C to stop. Start success.\n"); |
1001 | } |
1002 | } |
1003 | |
1004 | /** |
1005 | * @return string |
1006 | */ |
1007 | protected static function getVersionLine(): string |
1008 | { |
1009 | //Show version |
1010 | $jitStatus = function_exists('opcache_get_status') && (opcache_get_status()['jit']['on'] ?? false) === true ? 'on' : 'off'; |
1011 | $version = str_pad('Workerman/' . static::VERSION, 24); |
1012 | $version .= str_pad('PHP/' . PHP_VERSION . ' (JIT ' . $jitStatus . ')', 30); |
1013 | $version .= php_uname('s') . '/' . php_uname('r') . PHP_EOL; |
1014 | return $version; |
1015 | } |
1016 | |
1017 | /** |
1018 | * Get UI columns to be shown in terminal |
1019 | * |
1020 | * 1. $columnMap: ['ui_column_name' => 'clas_property_name'] |
1021 | * 2. Consider move into configuration in future |
1022 | * |
1023 | * @return array |
1024 | */ |
1025 | public static function getUiColumns(): array |
1026 | { |
1027 | return [ |
1028 | 'event-loop' => 'eventLoopName', |
1029 | 'proto' => 'transport', |
1030 | 'user' => 'user', |
1031 | 'worker' => 'name', |
1032 | 'socket' => 'statusSocket', |
1033 | 'count' => 'count', |
1034 | 'state' => 'statusState', |
1035 | ]; |
1036 | } |
1037 | |
1038 | /** |
1039 | * Get single line total length for ui |
1040 | * |
1041 | * @return int |
1042 | */ |
1043 | public static function getSingleLineTotalLength(): int |
1044 | { |
1045 | $totalLength = 0; |
1046 | |
1047 | foreach (static::getUiColumns() as $columnName => $prop) { |
1048 | $key = 'max' . ucfirst(strtolower($columnName)) . 'NameLength'; |
1049 | $totalLength += static::getUiColumnLength($key) + static::UI_SAFE_LENGTH; |
1050 | } |
1051 | |
1052 | //Keep beauty when show less columns |
1053 | !defined('LINE_VERSION_LENGTH') && define('LINE_VERSION_LENGTH', 0); |
1054 | $totalLength <= LINE_VERSION_LENGTH && $totalLength = LINE_VERSION_LENGTH; |
1055 | |
1056 | return $totalLength; |
1057 | } |
1058 | |
1059 | /** |
1060 | * Parse command. |
1061 | * |
1062 | * @return void |
1063 | */ |
1064 | protected static function parseCommand(): void |
1065 | { |
1066 | if (DIRECTORY_SEPARATOR !== '/') { |
1067 | return; |
1068 | } |
1069 | |
1070 | // Check argv; |
1071 | $startFile = basename(static::$startFile); |
1072 | $usage = "Usage: php yourfile <command> [mode]\nCommands: \nstart\t\tStart worker in DEBUG mode.\n\t\tUse mode -d to start in DAEMON mode.\nstop\t\tStop worker.\n\t\tUse mode -g to stop gracefully.\nrestart\t\tRestart workers.\n\t\tUse mode -d to start in DAEMON mode.\n\t\tUse mode -g to stop gracefully.\nreload\t\tReload codes.\n\t\tUse mode -g to reload gracefully.\nstatus\t\tGet worker status.\n\t\tUse mode -d to show live status.\nconnections\tGet worker connections.\n"; |
1073 | $availableCommands = [ |
1074 | 'start', |
1075 | 'stop', |
1076 | 'restart', |
1077 | 'reload', |
1078 | 'status', |
1079 | 'connections', |
1080 | ]; |
1081 | $availableMode = [ |
1082 | '-d', |
1083 | '-g' |
1084 | ]; |
1085 | $command = $mode = ''; |
1086 | foreach (static::getArgv() as $value) { |
1087 | if (!$command && in_array($value, $availableCommands)) { |
1088 | $command = $value; |
1089 | } |
1090 | if (!$mode && in_array($value, $availableMode)) { |
1091 | $mode = $value; |
1092 | } |
1093 | } |
1094 | |
1095 | if (!$command) { |
1096 | exit($usage); |
1097 | } |
1098 | |
1099 | // Start command. |
1100 | $modeStr = ''; |
1101 | if ($command === 'start') { |
1102 | if ($mode === '-d' || static::$daemonize) { |
1103 | $modeStr = 'in DAEMON mode'; |
1104 | } else { |
1105 | $modeStr = 'in DEBUG mode'; |
1106 | } |
1107 | } |
1108 | static::log("Workerman[$startFile] $command $modeStr"); |
1109 | |
1110 | // Get master process PID. |
1111 | $masterPid = is_file(static::$pidFile) ? (int)file_get_contents(static::$pidFile) : 0; |
1112 | // Master is still alive? |
1113 | if (static::checkMasterIsAlive($masterPid)) { |
1114 | if ($command === 'start') { |
1115 | static::log("Workerman[$startFile] already running"); |
1116 | exit; |
1117 | } |
1118 | } elseif ($command !== 'start' && $command !== 'restart') { |
1119 | static::log("Workerman[$startFile] not run"); |
1120 | exit; |
1121 | } |
1122 | |
1123 | // execute command. |
1124 | switch ($command) { |
1125 | case 'start': |
1126 | if ($mode === '-d') { |
1127 | static::$daemonize = true; |
1128 | } |
1129 | break; |
1130 | case 'status': |
1131 | // Delete status file on shutdown |
1132 | register_shutdown_function(unlink(...), static::$statisticsFile); |
1133 | while (1) { |
1134 | // Master process will send SIGIOT signal to all child processes. |
1135 | posix_kill($masterPid, SIGIOT); |
1136 | // Waiting a moment. |
1137 | sleep(1); |
1138 | // Clear terminal. |
1139 | if ($mode === '-d') { |
1140 | static::safeEcho("\33[H\33[2J\33(B\33[m", true); |
1141 | } |
1142 | // Echo status data. |
1143 | static::safeEcho(static::formatProcessStatusData()); |
1144 | if ($mode !== '-d') { |
1145 | exit(0); |
1146 | } |
1147 | static::safeEcho("\nPress Ctrl+C to quit.\n\n"); |
1148 | } |
1149 | case 'connections': |
1150 | // Delete status file on shutdown |
1151 | register_shutdown_function(unlink(...), static::$connectionsFile); |
1152 | // Master process will send SIGIO signal to all child processes. |
1153 | posix_kill($masterPid, SIGIO); |
1154 | // Waiting a moment. |
1155 | usleep(500000); |
1156 | // Display statistics data from a disk file. |
1157 | static::safeEcho(static::formatConnectionStatusData()); |
1158 | exit(0); |
1159 | case 'restart': |
1160 | case 'stop': |
1161 | if ($mode === '-g') { |
1162 | static::$gracefulStop = true; |
1163 | $sig = SIGQUIT; |
1164 | static::log("Workerman[$startFile] is gracefully stopping ..."); |
1165 | } else { |
1166 | static::$gracefulStop = false; |
1167 | $sig = SIGINT; |
1168 | static::log("Workerman[$startFile] is stopping ..."); |
1169 | } |
1170 | // Send stop signal to master process. |
1171 | $masterPid && posix_kill($masterPid, $sig); |
1172 | // Timeout. |
1173 | $timeout = static::$stopTimeout + 3; |
1174 | $startTime = time(); |
1175 | // Check master process is still alive? |
1176 | while (1) { |
1177 | $masterIsAlive = $masterPid && posix_kill($masterPid, 0); |
1178 | if ($masterIsAlive) { |
1179 | // Timeout? |
1180 | if (!static::getGracefulStop() && time() - $startTime >= $timeout) { |
1181 | static::log("Workerman[$startFile] stop fail"); |
1182 | exit; |
1183 | } |
1184 | // Waiting a moment. |
1185 | usleep(10000); |
1186 | continue; |
1187 | } |
1188 | // Stop success. |
1189 | static::log("Workerman[$startFile] stop success"); |
1190 | if ($command === 'stop') { |
1191 | exit(0); |
1192 | } |
1193 | if ($mode === '-d') { |
1194 | static::$daemonize = true; |
1195 | } |
1196 | break; |
1197 | } |
1198 | break; |
1199 | case 'reload': |
1200 | if ($mode === '-g') { |
1201 | $sig = SIGUSR2; |
1202 | } else { |
1203 | $sig = SIGUSR1; |
1204 | } |
1205 | posix_kill($masterPid, $sig); |
1206 | exit; |
1207 | default : |
1208 | static::safeEcho('Unknown command: ' . $command . "\n"); |
1209 | exit($usage); |
1210 | } |
1211 | } |
1212 | |
1213 | /** |
1214 | * Get argv. |
1215 | * |
1216 | * @return array |
1217 | */ |
1218 | public static function getArgv(): array |
1219 | { |
1220 | global $argv; |
1221 | return static::$command ? [...$argv, ...explode(' ', static::$command)] : $argv; |
1222 | } |
1223 | |
1224 | /** |
1225 | * Format status data. |
1226 | * |
1227 | * @return string |
1228 | */ |
1229 | protected static function formatProcessStatusData(): string |
1230 | { |
1231 | static $totalRequestCache = []; |
1232 | if (!is_readable(static::$statisticsFile)) { |
1233 | return ''; |
1234 | } |
1235 | $info = file(static::$statisticsFile, FILE_IGNORE_NEW_LINES); |
1236 | if (!$info) { |
1237 | return ''; |
1238 | } |
1239 | $statusStr = ''; |
1240 | $currentTotalRequest = []; |
1241 | $workerInfo = []; |
1242 | try { |
1243 | $workerInfo = unserialize($info[0], ['allowed_classes' => false]); |
1244 | } catch (Throwable) { |
1245 | // do nothing |
1246 | } |
1247 | if (!is_array($workerInfo)) { |
1248 | $workerInfo = []; |
1249 | } |
1250 | ksort($workerInfo, SORT_NUMERIC); |
1251 | unset($info[0]); |
1252 | $dataWaitingSort = []; |
1253 | $readProcessStatus = false; |
1254 | $totalRequests = 0; |
1255 | $totalQps = 0; |
1256 | $totalConnections = 0; |
1257 | $totalFails = 0; |
1258 | $totalMemory = 0; |
1259 | $totalTimers = 0; |
1260 | $maxLen1 = max(static::getUiColumnLength('maxSocketNameLength'), 2 * static::UI_SAFE_LENGTH); |
1261 | $maxLen2 = max(static::getUiColumnLength('maxWorkerNameLength'), 2 * static::UI_SAFE_LENGTH); |
1262 | foreach ($info as $value) { |
1263 | if (!$readProcessStatus) { |
1264 | $statusStr .= $value . "\n"; |
1265 | if (preg_match('/^pid.*?memory.*?listening/', $value)) { |
1266 | $readProcessStatus = true; |
1267 | } |
1268 | continue; |
1269 | } |
1270 | if (preg_match('/^[0-9]+/', $value, $pidMath)) { |
1271 | $pid = $pidMath[0]; |
1272 | $dataWaitingSort[$pid] = $value; |
1273 | if (preg_match('/^\S+?\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?(\S+?)\s+?/', $value, $match)) { |
1274 | $totalMemory += (float)str_ireplace('M', '', $match[1]); |
1275 | $maxLen1 = max($maxLen1, strlen($match[2])); |
1276 | $maxLen2 = max($maxLen2, strlen($match[3])); |
1277 | $totalConnections += (int)$match[4]; |
1278 | $totalFails += (int)$match[5]; |
1279 | $totalTimers += (int)$match[6]; |
1280 | $currentTotalRequest[$pid] = $match[7]; |
1281 | $totalRequests += (int)$match[7]; |
1282 | } |
1283 | } |
1284 | } |
1285 | foreach ($workerInfo as $pid => $info) { |
1286 | if (!isset($dataWaitingSort[$pid])) { |
1287 | $statusStr .= "$pid\t" . str_pad('N/A', 7) . " " |
1288 | . str_pad($info['listen'], $maxLen1) . " " |
1289 | . str_pad((string)$info['name'], $maxLen2) . " " |
1290 | . str_pad('N/A', 11) . " " . str_pad('N/A', 9) . " " |
1291 | . str_pad('N/A', 7) . " " . str_pad('N/A', 13) . " N/A [busy] \n"; |
1292 | continue; |
1293 | } |
1294 | //$qps = isset($totalRequestCache[$pid]) ? $currentTotalRequest[$pid] |
1295 | if (!isset($totalRequestCache[$pid], $currentTotalRequest[$pid])) { |
1296 | $qps = 0; |
1297 | } else { |
1298 | $qps = $currentTotalRequest[$pid] - $totalRequestCache[$pid]; |
1299 | $totalQps += $qps; |
1300 | } |
1301 | $statusStr .= $dataWaitingSort[$pid] . " " . str_pad((string)$qps, 6) . " [idle]\n"; |
1302 | } |
1303 | $totalRequestCache = $currentTotalRequest; |
1304 | $statusStr .= "---------------------------------------------------PROCESS STATUS--------------------------------------------------------\n"; |
1305 | $statusStr .= "Summary\t" . str_pad($totalMemory . 'M', 7) . " " |
1306 | . str_pad('-', $maxLen1) . " " |
1307 | . str_pad('-', $maxLen2) . " " |
1308 | . str_pad((string)$totalConnections, 11) . " " . str_pad((string)$totalFails, 9) . " " |
1309 | . str_pad((string)$totalTimers, 7) . " " . str_pad((string)$totalRequests, 13) . " " |
1310 | . str_pad((string)$totalQps, 6) . " [Summary] \n"; |
1311 | return $statusStr; |
1312 | } |
1313 | |
1314 | protected static function formatConnectionStatusData(): string |
1315 | { |
1316 | return file_get_contents(static::$connectionsFile); |
1317 | } |
1318 | |
1319 | /** |
1320 | * Install signal handler. |
1321 | * |
1322 | * @return void |
1323 | */ |
1324 | protected static function installSignal(): void |
1325 | { |
1326 | if (DIRECTORY_SEPARATOR !== '/') { |
1327 | return; |
1328 | } |
1329 | $signals = [SIGINT, SIGTERM, SIGHUP, SIGTSTP, SIGQUIT, SIGUSR1, SIGUSR2, SIGIOT, SIGIO]; |
1330 | foreach ($signals as $signal) { |
1331 | pcntl_signal($signal, static::signalHandler(...), false); |
1332 | } |
1333 | // ignore |
1334 | pcntl_signal(SIGPIPE, SIG_IGN, false); |
1335 | } |
1336 | |
1337 | /** |
1338 | * Reinstall signal handler. |
1339 | * |
1340 | * @return void |
1341 | */ |
1342 | protected static function reinstallSignal(): void |
1343 | { |
1344 | if (DIRECTORY_SEPARATOR !== '/') { |
1345 | return; |
1346 | } |
1347 | $signals = [SIGINT, SIGTERM, SIGHUP, SIGTSTP, SIGQUIT, SIGUSR1, SIGUSR2, SIGIOT, SIGIO]; |
1348 | foreach ($signals as $signal) { |
1349 | // Rewrite master process signal. |
1350 | static::$globalEvent->onSignal($signal, static::signalHandler(...)); |
1351 | } |
1352 | } |
1353 | |
1354 | /** |
1355 | * Signal handler. |
1356 | * |
1357 | * @param int $signal |
1358 | */ |
1359 | protected static function signalHandler(int $signal): void |
1360 | { |
1361 | switch ($signal) { |
1362 | // Stop. |
1363 | case SIGINT: |
1364 | case SIGTERM: |
1365 | case SIGHUP: |
1366 | case SIGTSTP: |
1367 | static::$gracefulStop = false; |
1368 | static::stopAll(0, 'received signal ' . static::getSignalName($signal)); |
1369 | break; |
1370 | // Graceful stop. |
1371 | case SIGQUIT: |
1372 | static::$gracefulStop = true; |
1373 | static::stopAll(0, 'received signal ' . static::getSignalName($signal)); |
1374 | break; |
1375 | // Reload. |
1376 | case SIGUSR2: |
1377 | case SIGUSR1: |
1378 | if (static::$status === static::STATUS_RELOADING || static::$status === static::STATUS_SHUTDOWN) { |
1379 | return; |
1380 | } |
1381 | static::$gracefulStop = $signal === SIGUSR2; |
1382 | static::$pidsToRestart = static::getAllWorkerPids(); |
1383 | static::reload(); |
1384 | break; |
1385 | // Show status. |
1386 | case SIGIOT: |
1387 | static::writeStatisticsToStatusFile(); |
1388 | break; |
1389 | // Show connection status. |
1390 | case SIGIO: |
1391 | static::writeConnectionsStatisticsToStatusFile(); |
1392 | break; |
1393 | } |
1394 | } |
1395 | |
1396 | /** |
1397 | * Get signal name. |
1398 | * |
1399 | * @param int $signal |
1400 | * @return string |
1401 | */ |
1402 | protected static function getSignalName(int $signal): string |
1403 | { |
1404 | return match ($signal) { |
1405 | SIGINT => 'SIGINT', |
1406 | SIGTERM => 'SIGTERM', |
1407 | SIGHUP => 'SIGHUP', |
1408 | SIGTSTP => 'SIGTSTP', |
1409 | SIGQUIT => 'SIGQUIT', |
1410 | SIGUSR1 => 'SIGUSR1', |
1411 | SIGUSR2 => 'SIGUSR2', |
1412 | SIGIOT => 'SIGIOT', |
1413 | SIGIO => 'SIGIO', |
1414 | default => $signal, |
1415 | }; |
1416 | } |
1417 | |
1418 | /** |
1419 | * Run as daemon mode. |
1420 | */ |
1421 | protected static function daemonize(): void |
1422 | { |
1423 | if (!static::$daemonize || DIRECTORY_SEPARATOR !== '/') { |
1424 | return; |
1425 | } |
1426 | umask(0); |
1427 | $pid = pcntl_fork(); |
1428 | if (-1 === $pid) { |
1429 | throw new RuntimeException('Fork fail'); |
1430 | } elseif ($pid > 0) { |
1431 | exit(0); |
1432 | } |
1433 | if (-1 === posix_setsid()) { |
1434 | throw new RuntimeException("Setsid fail"); |
1435 | } |
1436 | // Fork again avoid SVR4 system regain the control of terminal. |
1437 | $pid = pcntl_fork(); |
1438 | if (-1 === $pid) { |
1439 | throw new RuntimeException("Fork fail"); |
1440 | } elseif (0 !== $pid) { |
1441 | exit(0); |
1442 | } |
1443 | } |
1444 | |
1445 | /** |
1446 | * Redirect standard output to stdoutFile. |
1447 | * |
1448 | * @return void |
1449 | */ |
1450 | public static function resetStd(): void |
1451 | { |
1452 | if (!static::$daemonize || DIRECTORY_SEPARATOR !== '/') { |
1453 | return; |
1454 | } |
1455 | |
1456 | if (is_resource(STDOUT)) { |
1457 | fclose(STDOUT); |
1458 | } |
1459 | |
1460 | if (is_resource(STDERR)) { |
1461 | fclose(STDERR); |
1462 | } |
1463 | |
1464 | if (is_resource(static::$outputStream)) { |
1465 | fclose(static::$outputStream); |
1466 | } |
1467 | |
1468 | set_error_handler(static fn (): bool => true); |
1469 | $stdOutStream = fopen(static::$stdoutFile, 'a'); |
1470 | restore_error_handler(); |
1471 | |
1472 | if ($stdOutStream === false) { |
1473 | return; |
1474 | } |
1475 | |
1476 | static::$outputStream = $stdOutStream; |
1477 | |
1478 | // Fix standard output cannot redirect of PHP 8.1.8's bug |
1479 | if (function_exists('posix_isatty') && posix_isatty(2)) { |
1480 | ob_start(function (string $string) { |
1481 | file_put_contents(static::$stdoutFile, $string, FILE_APPEND); |
1482 | }, 1); |
1483 | } |
1484 | } |
1485 | |
1486 | /** |
1487 | * Save pid. |
1488 | */ |
1489 | protected static function saveMasterPid(): void |
1490 | { |
1491 | if (DIRECTORY_SEPARATOR !== '/') { |
1492 | return; |
1493 | } |
1494 | |
1495 | static::$masterPid = posix_getpid(); |
1496 | if (false === file_put_contents(static::$pidFile, static::$masterPid)) { |
1497 | throw new RuntimeException('can not save pid to ' . static::$pidFile); |
1498 | } |
1499 | } |
1500 | |
1501 | /** |
1502 | * Get all pids of worker processes. |
1503 | * |
1504 | * @return array |
1505 | */ |
1506 | protected static function getAllWorkerPids(): array |
1507 | { |
1508 | $pidArray = []; |
1509 | foreach (static::$pidMap as $workerPidArray) { |
1510 | foreach ($workerPidArray as $workerPid) { |
1511 | $pidArray[$workerPid] = $workerPid; |
1512 | } |
1513 | } |
1514 | return $pidArray; |
1515 | } |
1516 | |
1517 | /** |
1518 | * Fork some worker processes. |
1519 | * |
1520 | * @return void |
1521 | */ |
1522 | protected static function forkWorkers(): void |
1523 | { |
1524 | if (DIRECTORY_SEPARATOR === '/') { |
1525 | static::forkWorkersForLinux(); |
1526 | } else { |
1527 | static::forkWorkersForWindows(); |
1528 | } |
1529 | } |
1530 | |
1531 | /** |
1532 | * Fork some worker processes. |
1533 | * |
1534 | * @return void |
1535 | */ |
1536 | protected static function forkWorkersForLinux(): void |
1537 | { |
1538 | foreach (static::$workers as $worker) { |
1539 | if (static::$status === static::STATUS_STARTING) { |
1540 | if (empty($worker->name)) { |
1541 | $worker->name = $worker->getSocketName(); |
1542 | } |
1543 | } |
1544 | while (count(static::$pidMap[$worker->workerId]) < $worker->count) { |
1545 | static::forkOneWorkerForLinux($worker); |
1546 | } |
1547 | } |
1548 | } |
1549 | |
1550 | /** |
1551 | * Fork some worker processes. |
1552 | * |
1553 | * @return void |
1554 | */ |
1555 | protected static function forkWorkersForWindows(): void |
1556 | { |
1557 | $files = static::getStartFilesForWindows(); |
1558 | if (count($files) === 1 || in_array('-q', static::getArgv())) { |
1559 | if (count(static::$workers) > 1) { |
1560 | static::safeEcho("@@@ Error: multi workers init in one php file are not support @@@\r\n"); |
1561 | static::safeEcho("@@@ See https://www.workerman.net/doc/workerman/faq/multi-woker-for-windows.html @@@\r\n"); |
1562 | } elseif (count(static::$workers) <= 0) { |
1563 | exit("@@@no worker inited@@@\r\n\r\n"); |
1564 | } |
1565 | |
1566 | reset(static::$workers); |
1567 | /** @var Worker $worker */ |
1568 | $worker = current(static::$workers); |
1569 | |
1570 | Timer::delAll(); |
1571 | |
1572 | //Update process state. |
1573 | static::$status = static::STATUS_RUNNING; |
1574 | |
1575 | // Register shutdown function for checking errors. |
1576 | register_shutdown_function(static::checkErrors(...)); |
1577 | |
1578 | // Create a global event loop. |
1579 | if (static::$globalEvent === null) { |
1580 | static::$eventLoopClass = $worker->eventLoop ?: static::$eventLoopClass; |
1581 | static::$globalEvent = new static::$eventLoopClass(); |
1582 | static::$globalEvent->setErrorHandler(function ($exception) { |
1583 | static::stopAll(250, $exception); |
1584 | }); |
1585 | } |
1586 | |
1587 | // Reinstall signal. |
1588 | static::reinstallSignal(); |
1589 | |
1590 | // Init Timer. |
1591 | Timer::init(static::$globalEvent); |
1592 | |
1593 | restore_error_handler(); |
1594 | |
1595 | // Add an empty timer to prevent the event-loop from exiting. |
1596 | Timer::add(1000000, function (){}); |
1597 | |
1598 | // Display UI. |
1599 | static::safeEcho(str_pad($worker->name, 48) . str_pad($worker->getSocketName(), 36) . str_pad('1', 10) . " [ok]\n"); |
1600 | $worker->listen(); |
1601 | $worker->run(); |
1602 | static::$globalEvent->run(); |
1603 | if (static::$status !== self::STATUS_SHUTDOWN) { |
1604 | $err = new RuntimeException('event-loop exited'); |
1605 | static::log($err); |
1606 | exit(250); |
1607 | } |
1608 | exit(0); |
1609 | } |
1610 | |
1611 | static::$globalEvent = new Select(); |
1612 | static::$globalEvent->setErrorHandler(function ($exception) { |
1613 | static::stopAll(250, $exception); |
1614 | }); |
1615 | Timer::init(static::$globalEvent); |
1616 | foreach ($files as $startFile) { |
1617 | static::forkOneWorkerForWindows($startFile); |
1618 | } |
1619 | } |
1620 | |
1621 | /** |
1622 | * Get start files for windows. |
1623 | * |
1624 | * @return array |
1625 | */ |
1626 | public static function getStartFilesForWindows(): array |
1627 | { |
1628 | $files = []; |
1629 | foreach (static::getArgv() as $file) { |
1630 | if (is_file($file)) { |
1631 | $files[$file] = $file; |
1632 | } |
1633 | } |
1634 | return $files; |
1635 | } |
1636 | |
1637 | /** |
1638 | * Fork one worker process. |
1639 | * |
1640 | * @param string $startFile |
1641 | */ |
1642 | public static function forkOneWorkerForWindows(string $startFile): void |
1643 | { |
1644 | $startFile = realpath($startFile); |
1645 | $descriptorSpec = [STDIN, STDOUT, STDOUT]; |
1646 | $pipes = []; |
1647 | $process = proc_open('"' . PHP_BINARY . '" ' . " \"$startFile\" -q", $descriptorSpec, $pipes, null, null, ['bypass_shell' => true]); |
1648 | |
1649 | if (static::$globalEvent === null) { |
1650 | static::$globalEvent = new Select(); |
1651 | static::$globalEvent->setErrorHandler(function ($exception) { |
1652 | static::stopAll(250, $exception); |
1653 | }); |
1654 | Timer::init(static::$globalEvent); |
1655 | } |
1656 | |
1657 | // 保存子进程句柄 |
1658 | static::$processForWindows[$startFile] = [$process, $startFile]; |
1659 | } |
1660 | |
1661 | /** |
1662 | * check worker status for windows. |
1663 | * |
1664 | * @return void |
1665 | */ |
1666 | protected static function checkWorkerStatusForWindows(): void |
1667 | { |
1668 | foreach (static::$processForWindows as $processData) { |
1669 | $process = $processData[0]; |
1670 | $startFile = $processData[1]; |
1671 | $status = proc_get_status($process); |
1672 | if (!$status['running']) { |
1673 | static::safeEcho("process $startFile terminated and try to restart\n"); |
1674 | proc_close($process); |
1675 | static::forkOneWorkerForWindows($startFile); |
1676 | } |
1677 | } |
1678 | } |
1679 | |
1680 | /** |
1681 | * Fork one worker process. |
1682 | * |
1683 | * @param self $worker |
1684 | */ |
1685 | protected static function forkOneWorkerForLinux(self $worker): void |
1686 | { |
1687 | // Get available worker id. |
1688 | $id = static::getId($worker->workerId, 0); |
1689 | $pid = pcntl_fork(); |
1690 | // For master process. |
1691 | if ($pid > 0) { |
1692 | static::$pidMap[$worker->workerId][$pid] = $pid; |
1693 | static::$idMap[$worker->workerId][$id] = $pid; |
1694 | } // For child processes. |
1695 | elseif (0 === $pid) { |
1696 | srand(); |
1697 | mt_srand(); |
1698 | static::$gracefulStop = false; |
1699 | if (static::$status === static::STATUS_STARTING) { |
1700 | static::resetStd(); |
1701 | } |
1702 | static::$pidsToRestart = static::$pidMap = []; |
1703 | // Remove other listener. |
1704 | foreach (static::$workers as $key => $oneWorker) { |
1705 | if ($oneWorker->workerId !== $worker->workerId) { |
1706 | $oneWorker->unlisten(); |
1707 | unset(static::$workers[$key]); |
1708 | } |
1709 | } |
1710 | Timer::delAll(); |
1711 | |
1712 | //Update process state. |
1713 | static::$status = static::STATUS_RUNNING; |
1714 | |
1715 | // Register shutdown function for checking errors. |
1716 | register_shutdown_function(static::checkErrors(...)); |
1717 | |
1718 | // Create a global event loop. |
1719 | if (static::$globalEvent === null) { |
1720 | static::$eventLoopClass = $worker->eventLoop ?: static::$eventLoopClass; |
1721 | static::$globalEvent = new static::$eventLoopClass(); |
1722 | static::$globalEvent->setErrorHandler(function ($exception) { |
1723 | static::stopAll(250, $exception); |
1724 | }); |
1725 | } |
1726 | |
1727 | // Reinstall signal. |
1728 | static::reinstallSignal(); |
1729 | |
1730 | // Init Timer. |
1731 | Timer::init(static::$globalEvent); |
1732 | |
1733 | restore_error_handler(); |
1734 | |
1735 | static::setProcessTitle('WorkerMan: worker process ' . $worker->name . ' ' . $worker->getSocketName()); |
1736 | $worker->setUserAndGroup(); |
1737 | $worker->id = $id; |
1738 | $worker->run(); |
1739 | // Main loop. |
1740 | static::$globalEvent->run(); |
1741 | if (static::$status !== self::STATUS_SHUTDOWN) { |
1742 | $err = new Exception('event-loop exited'); |
1743 | static::log($err); |
1744 | exit(250); |
1745 | } |
1746 | exit(0); |
1747 | } else { |
1748 | throw new RuntimeException("forkOneWorker fail"); |
1749 | } |
1750 | } |
1751 | |
1752 | /** |
1753 | * Get worker id. |
1754 | * |
1755 | * @param string $workerId |
1756 | * @param int $pid |
1757 | * @return false|int|string |
1758 | */ |
1759 | protected static function getId(string $workerId, int $pid): false|int|string |
1760 | { |
1761 | return array_search($pid, static::$idMap[$workerId]); |
1762 | } |
1763 | |
1764 | /** |
1765 | * Set unix user and group for current process. |
1766 | * |
1767 | * @return void |
1768 | */ |
1769 | public function setUserAndGroup(): void |
1770 | { |
1771 | // Get uid. |
1772 | $userInfo = posix_getpwnam($this->user); |
1773 | if (!$userInfo) { |
1774 | static::log("Warning: User $this->user not exists"); |
1775 | return; |
1776 | } |
1777 | $uid = $userInfo['uid']; |
1778 | // Get gid. |
1779 | if ($this->group) { |
1780 | $groupInfo = posix_getgrnam($this->group); |
1781 | if (!$groupInfo) { |
1782 | static::log("Warning: Group $this->group not exists"); |
1783 | return; |
1784 | } |
1785 | $gid = $groupInfo['gid']; |
1786 | } else { |
1787 | $gid = $userInfo['gid']; |
1788 | } |
1789 | |
1790 | // Set uid and gid. |
1791 | if ($uid !== posix_getuid() || $gid !== posix_getgid()) { |
1792 | if (!posix_setgid($gid) || !posix_initgroups($userInfo['name'], $gid) || !posix_setuid($uid)) { |
1793 | static::log("Warning: change gid or uid fail."); |
1794 | } |
1795 | } |
1796 | } |
1797 | |
1798 | /** |
1799 | * Set process name. |
1800 | * |
1801 | * @param string $title |
1802 | * @return void |
1803 | */ |
1804 | protected static function setProcessTitle(string $title): void |
1805 | { |
1806 | set_error_handler(static fn (): bool => true); |
1807 | cli_set_process_title($title); |
1808 | restore_error_handler(); |
1809 | } |
1810 | |
1811 | /** |
1812 | * Monitor all child processes. |
1813 | * |
1814 | * @return void |
1815 | * @throws Throwable |
1816 | */ |
1817 | protected static function monitorWorkers(): void |
1818 | { |
1819 | if (DIRECTORY_SEPARATOR === '/') { |
1820 | static::monitorWorkersForLinux(); |
1821 | } else { |
1822 | static::monitorWorkersForWindows(); |
1823 | } |
1824 | } |
1825 | |
1826 | /** |
1827 | * Monitor all child processes. |
1828 | * |
1829 | * @return void |
1830 | */ |
1831 | protected static function monitorWorkersForLinux(): void |
1832 | { |
1833 | static::$status = static::STATUS_RUNNING; |
1834 | // @phpstan-ignore-next-line While loop condition is always true. |
1835 | while (1) { |
1836 | // Calls signal handlers for pending signals. |
1837 | pcntl_signal_dispatch(); |
1838 | // Suspends execution of the current process until a child has exited, or until a signal is delivered |
1839 | $status = 0; |
1840 | $pid = pcntl_wait($status, WUNTRACED); |
1841 | // Calls signal handlers for pending signals again. |
1842 | pcntl_signal_dispatch(); |
1843 | // If a child has already exited. |
1844 | if ($pid > 0) { |
1845 | // Find out which worker process exited. |
1846 | foreach (static::$pidMap as $workerId => $workerPidArray) { |
1847 | if (isset($workerPidArray[$pid])) { |
1848 | $worker = static::$workers[$workerId]; |
1849 | // Fix exit with status 2 for php8.2 |
1850 | if ($status === SIGINT && static::$status === static::STATUS_SHUTDOWN) { |
1851 | $status = 0; |
1852 | } |
1853 | // Exit status. |
1854 | if ($status !== 0) { |
1855 | static::log("worker[$worker->name:$pid] exit with status $status"); |
1856 | } |
1857 | |
1858 | // onWorkerExit |
1859 | if (static::$onWorkerExit) { |
1860 | try { |
1861 | (static::$onWorkerExit)($worker, $status, $pid); |
1862 | } catch (Throwable $exception) { |
1863 | static::log("worker[$worker->name] onWorkerExit $exception"); |
1864 | } |
1865 | } |
1866 | |
1867 | // For Statistics. |
1868 | static::$globalStatistics['worker_exit_info'][$workerId][$status] ??= 0; |
1869 | static::$globalStatistics['worker_exit_info'][$workerId][$status]++; |
1870 | |
1871 | // Clear process data. |
1872 | unset(static::$pidMap[$workerId][$pid]); |
1873 | |
1874 | // Mark id is available. |
1875 | $id = static::getId($workerId, $pid); |
1876 | static::$idMap[$workerId][$id] = 0; |
1877 | |
1878 | break; |
1879 | } |
1880 | } |
1881 | // Is still running state then fork a new worker process. |
1882 | if (static::$status !== static::STATUS_SHUTDOWN) { |
1883 | static::forkWorkers(); |
1884 | // If reloading continue. |
1885 | if (isset(static::$pidsToRestart[$pid])) { |
1886 | unset(static::$pidsToRestart[$pid]); |
1887 | static::reload(); |
1888 | } |
1889 | } |
1890 | } |
1891 | |
1892 | // If shutdown state and all child processes exited, then master process exit. |
1893 | if (static::$status === static::STATUS_SHUTDOWN && empty(static::getAllWorkerPids())) { |
1894 | static::exitAndClearAll(); |
1895 | } |
1896 | } |
1897 | } |
1898 | |
1899 | /** |
1900 | * Monitor all child processes. |
1901 | * |
1902 | * @return void |
1903 | */ |
1904 | protected static function monitorWorkersForWindows(): void |
1905 | { |
1906 | Timer::add(1, static::checkWorkerStatusForWindows(...)); |
1907 | |
1908 | static::$globalEvent->run(); |
1909 | } |
1910 | |
1911 | /** |
1912 | * Exit current process. |
1913 | */ |
1914 | protected static function exitAndClearAll(): void |
1915 | { |
1916 | clearstatcache(); |
1917 | foreach (static::$workers as $worker) { |
1918 | $socketName = $worker->getSocketName(); |
1919 | if ($worker->transport === 'unix' && $socketName) { |
1920 | [, $address] = explode(':', $socketName, 2); |
1921 | $address = substr($address, strpos($address, '/') + 2); |
1922 | if (file_exists($address)) { |
1923 | @unlink($address); |
1924 | } |
1925 | } |
1926 | } |
1927 | if (file_exists(static::$pidFile)) { |
1928 | @unlink(static::$pidFile); |
1929 | } |
1930 | static::log("Workerman[" . basename(static::$startFile) . "] has been stopped"); |
1931 | if (static::$onMasterStop) { |
1932 | (static::$onMasterStop)(); |
1933 | } |
1934 | exit(0); |
1935 | } |
1936 | |
1937 | /** |
1938 | * Execute reload. |
1939 | * |
1940 | * @return void |
1941 | */ |
1942 | protected static function reload(): void |
1943 | { |
1944 | // For master process. |
1945 | if (static::$masterPid === posix_getpid()) { |
1946 | $sig = static::getGracefulStop() ? SIGUSR2 : SIGUSR1; |
1947 | // Set reloading state. |
1948 | if (static::$status === static::STATUS_RUNNING) { |
1949 | static::log("Workerman[" . basename(static::$startFile) . "] reloading"); |
1950 | static::$status = static::STATUS_RELOADING; |
1951 | |
1952 | static::resetStd(); |
1953 | // Try to emit onMasterReload callback. |
1954 | if (static::$onMasterReload) { |
1955 | try { |
1956 | (static::$onMasterReload)(); |
1957 | } catch (Throwable $e) { |
1958 | static::stopAll(250, $e); |
1959 | } |
1960 | static::initId(); |
1961 | } |
1962 | |
1963 | // Send reload signal to all child processes. |
1964 | $reloadablePidArray = []; |
1965 | foreach (static::$pidMap as $workerId => $workerPidArray) { |
1966 | $worker = static::$workers[$workerId]; |
1967 | if ($worker->reloadable) { |
1968 | $reloadablePidArray += $workerPidArray; |
1969 | continue; |
1970 | } |
1971 | // Send reload signal to a worker process which reloadable is false. |
1972 | array_walk($workerPidArray, static fn ($pid) => posix_kill($pid, $sig)); |
1973 | } |
1974 | // Get all pids that are waiting reload. |
1975 | static::$pidsToRestart = array_intersect(static::$pidsToRestart, $reloadablePidArray); |
1976 | } |
1977 | |
1978 | // Reload complete. |
1979 | if (empty(static::$pidsToRestart)) { |
1980 | if (static::$status !== static::STATUS_SHUTDOWN) { |
1981 | static::$status = static::STATUS_RUNNING; |
1982 | } |
1983 | return; |
1984 | } |
1985 | // Continue reload. |
1986 | $oneWorkerPid = current(static::$pidsToRestart); |
1987 | // Send reload signal to a worker process. |
1988 | posix_kill($oneWorkerPid, $sig); |
1989 | // If the process does not exit after stopTimeout seconds try to kill it. |
1990 | if (!static::getGracefulStop()) { |
1991 | Timer::add(static::$stopTimeout, posix_kill(...), [$oneWorkerPid, SIGKILL], false); |
1992 | } |
1993 | } // For child processes. |
1994 | else { |
1995 | reset(static::$workers); |
1996 | $worker = current(static::$workers); |
1997 | // Try to emit onWorkerReload callback. |
1998 | if ($worker->onWorkerReload) { |
1999 | try { |
2000 | ($worker->onWorkerReload)($worker); |
2001 | } catch (Throwable $e) { |
2002 | static::stopAll(250, $e); |
2003 | } |
2004 | } |
2005 | |
2006 | if ($worker->reloadable) { |
2007 | static::stopAll(); |
2008 | } else { |
2009 | static::resetStd(); |
2010 | } |
2011 | } |
2012 | } |
2013 | |
2014 | /** |
2015 | * Stop all. |
2016 | * |
2017 | * @param int $code |
2018 | * @param mixed $log |
2019 | */ |
2020 | public static function stopAll(int $code = 0, mixed $log = ''): void |
2021 | { |
2022 | static::$status = static::STATUS_SHUTDOWN; |
2023 | // For master process. |
2024 | if (DIRECTORY_SEPARATOR === '/' && static::$masterPid === posix_getpid()) { |
2025 | if ($log) { |
2026 | static::log("Workerman[" . basename(static::$startFile) . "] $log"); |
2027 | } |
2028 | static::log("Workerman[" . basename(static::$startFile) . "] stopping" . ($code ? ", code [$code]" : '')); |
2029 | $workerPidArray = static::getAllWorkerPids(); |
2030 | // Send stop signal to all child processes. |
2031 | $sig = static::getGracefulStop() ? SIGQUIT : SIGINT; |
2032 | foreach ($workerPidArray as $workerPid) { |
2033 | // Fix exit with status 2 for php8.2 |
2034 | if ($sig === SIGINT && !static::$daemonize) { |
2035 | Timer::add(1, posix_kill(...), [$workerPid, SIGINT], false); |
2036 | } else { |
2037 | posix_kill($workerPid, $sig); |
2038 | } |
2039 | if (!static::getGracefulStop()) { |
2040 | Timer::add(ceil(static::$stopTimeout), posix_kill(...), [$workerPid, SIGKILL], false); |
2041 | } |
2042 | } |
2043 | Timer::add(1, static::checkIfChildRunning(...)); |
2044 | } // For child processes. |
2045 | else { |
2046 | if ($code && $log) { |
2047 | static::log($log); |
2048 | } |
2049 | // Execute exit. |
2050 | $workers = array_reverse(static::$workers); |
2051 | array_walk($workers, static fn (Worker $worker) => $worker->stop(false)); |
2052 | |
2053 | $callback = function () use ($code, $workers) { |
2054 | $allWorkerConnectionClosed = true; |
2055 | if (!static::getGracefulStop()) { |
2056 | foreach ($workers as $worker) { |
2057 | foreach ($worker->connections as $connection) { |
2058 | // Delay closing, waiting for data to be sent. |
2059 | if (!$connection->getRecvBufferQueueSize() && !isset($connection->context->closeTimer)) { |
2060 | $connection->context->closeTimer = Timer::delay(0.01, static fn () => $connection->close()); |
2061 | } |
2062 | $allWorkerConnectionClosed = false; |
2063 | } |
2064 | } |
2065 | } |
2066 | if ((!static::getGracefulStop() && $allWorkerConnectionClosed) || ConnectionInterface::$statistics['connection_count'] <= 0) { |
2067 | static::$globalEvent?->stop(); |
2068 | try { |
2069 | // Ignore Swoole ExitException: Swoole exit. |
2070 | exit($code); |
2071 | /** @phpstan-ignore-next-line */ |
2072 | } catch (Throwable) { |
2073 | // do nothing |
2074 | } |
2075 | } |
2076 | }; |
2077 | Timer::repeat(0.01, $callback); |
2078 | } |
2079 | } |
2080 | |
2081 | /** |
2082 | * check if child processes is really running |
2083 | */ |
2084 | protected static function checkIfChildRunning(): void |
2085 | { |
2086 | foreach (static::$pidMap as $workerId => $workerPidArray) { |
2087 | foreach ($workerPidArray as $pid => $workerPid) { |
2088 | if (!posix_kill($pid, 0)) { |
2089 | unset(static::$pidMap[$workerId][$pid]); |
2090 | } |
2091 | } |
2092 | } |
2093 | } |
2094 | |
2095 | /** |
2096 | * Get process status. |
2097 | * |
2098 | * @return int |
2099 | */ |
2100 | public static function getStatus(): int |
2101 | { |
2102 | return static::$status; |
2103 | } |
2104 | |
2105 | /** |
2106 | * If stop gracefully. |
2107 | * |
2108 | * @return bool |
2109 | */ |
2110 | public static function getGracefulStop(): bool |
2111 | { |
2112 | return static::$gracefulStop; |
2113 | } |
2114 | |
2115 | /** |
2116 | * |
2117 | * Write statistics data to disk. |
2118 | * |
2119 | * @return void |
2120 | */ |
2121 | protected static function writeStatisticsToStatusFile(): void |
2122 | { |
2123 | // For master process. |
2124 | if (static::$masterPid === posix_getpid()) { |
2125 | $allWorkerInfo = []; |
2126 | foreach (static::$pidMap as $workerId => $pidArray) { |
2127 | $worker = static::$workers[$workerId]; |
2128 | foreach ($pidArray as $pid) { |
2129 | $allWorkerInfo[$pid] = ['name' => $worker->name, 'listen' => $worker->getSocketName()]; |
2130 | } |
2131 | } |
2132 | file_put_contents(static::$statisticsFile, ''); |
2133 | chmod(static::$statisticsFile, 0722); |
2134 | file_put_contents(static::$statisticsFile, serialize($allWorkerInfo) . "\n", FILE_APPEND); |
2135 | $loadavg = function_exists('sys_getloadavg') ? array_map(round(...), sys_getloadavg(), [2, 2, 2]) : ['-', '-', '-']; |
2136 | file_put_contents(static::$statisticsFile, |
2137 | (static::$daemonize ? "Start worker in DAEMON mode." : "Start worker in DEBUG mode.") . "\n", FILE_APPEND); |
2138 | file_put_contents(static::$statisticsFile, |
2139 | "---------------------------------------------------GLOBAL STATUS---------------------------------------------------------\n", FILE_APPEND); |
2140 | file_put_contents(static::$statisticsFile, static::getVersionLine(), FILE_APPEND); |
2141 | file_put_contents(static::$statisticsFile, 'start time:' . date('Y-m-d H:i:s', |
2142 | static::$globalStatistics['start_timestamp']) |
2143 | . ' run ' . floor((time() - static::$globalStatistics['start_timestamp']) / (24 * 60 * 60)) |
2144 | . ' days ' . floor(((time() - static::$globalStatistics['start_timestamp']) % (24 * 60 * 60)) / (60 * 60)) |
2145 | . " hours " . 'load average: ' . implode(", ", $loadavg) . "\n", FILE_APPEND); |
2146 | file_put_contents(static::$statisticsFile, |
2147 | count(static::$pidMap) . ' workers ' . count(static::getAllWorkerPids()) . " processes\n", |
2148 | FILE_APPEND); |
2149 | file_put_contents(static::$statisticsFile, |
2150 | str_pad('name', static::getUiColumnLength('maxWorkerNameLength')) . " event-loop exit_status exit_count\n", FILE_APPEND); |
2151 | foreach (static::$pidMap as $workerId => $workerPidArray) { |
2152 | $worker = static::$workers[$workerId]; |
2153 | if (isset(static::$globalStatistics['worker_exit_info'][$workerId])) { |
2154 | foreach (static::$globalStatistics['worker_exit_info'][$workerId] as $workerExitStatus => $workerExitCount) { |
2155 | file_put_contents(static::$statisticsFile, |
2156 | str_pad($worker->name, static::getUiColumnLength('maxWorkerNameLength')) . " " . |
2157 | str_pad($worker->context->eventLoopName, 14) . " " . |
2158 | str_pad((string)$workerExitStatus, 16) . str_pad((string)$workerExitCount, 16) . "\n", FILE_APPEND); |
2159 | } |
2160 | } else { |
2161 | file_put_contents(static::$statisticsFile, |
2162 | str_pad($worker->name, static::getUiColumnLength('maxWorkerNameLength')) . " " . |
2163 | str_pad($worker->context->eventLoopName, 14) . " " . |
2164 | str_pad('0', 16) . str_pad('0', 16) . "\n", FILE_APPEND); |
2165 | } |
2166 | } |
2167 | file_put_contents(static::$statisticsFile, |
2168 | "---------------------------------------------------PROCESS STATUS--------------------------------------------------------\n", |
2169 | FILE_APPEND); |
2170 | file_put_contents(static::$statisticsFile, |
2171 | "pid\tmemory " . str_pad('listening', static::getUiColumnLength('maxSocketNameLength')) . " " . str_pad('name', |
2172 | static::getUiColumnLength('maxWorkerNameLength')) . " connections " . str_pad('send_fail', 9) . " " |
2173 | . str_pad('timers', 8) . str_pad('total_request', 13) . " qps status\n", FILE_APPEND); |
2174 | |
2175 | foreach (static::getAllWorkerPids() as $workerPid) { |
2176 | posix_kill($workerPid, SIGIOT); |
2177 | } |
2178 | return; |
2179 | } |
2180 | |
2181 | reset(static::$workers); |
2182 | /** @var static $worker */ |
2183 | $worker = current(static::$workers); |
2184 | $workerStatusStr = posix_getpid() . "\t" . str_pad(round(memory_get_usage() / (1024 * 1024), 2) . "M", 7) |
2185 | . " " . str_pad($worker->getSocketName(), static::getUiColumnLength('maxSocketNameLength')) . " " |
2186 | . str_pad(($worker->name === $worker->getSocketName() ? 'none' : $worker->name), static::getUiColumnLength('maxWorkerNameLength')) |
2187 | . " "; |
2188 | $workerStatusStr .= str_pad((string)ConnectionInterface::$statistics['connection_count'], 11) |
2189 | . " " . str_pad((string)ConnectionInterface::$statistics['send_fail'], 9) |
2190 | . " " . str_pad((string)static::$globalEvent->getTimerCount(), 7) |
2191 | . " " . str_pad((string)ConnectionInterface::$statistics['total_request'], 13) . "\n"; |
2192 | file_put_contents(static::$statisticsFile, $workerStatusStr, FILE_APPEND); |
2193 | } |
2194 | |
2195 | /** |
2196 | * Get UI column length |
2197 | * |
2198 | * @param $name |
2199 | * @return int |
2200 | */ |
2201 | protected static function getUiColumnLength($name): int |
2202 | { |
2203 | return static::$uiLengthData[$name] ?? 0; |
2204 | } |
2205 | |
2206 | /** |
2207 | * Write statistics data to disk. |
2208 | * |
2209 | * @return void |
2210 | */ |
2211 | protected static function writeConnectionsStatisticsToStatusFile(): void |
2212 | { |
2213 | // For master process. |
2214 | if (static::$masterPid === posix_getpid()) { |
2215 | file_put_contents(static::$connectionsFile, ''); |
2216 | chmod(static::$connectionsFile, 0722); |
2217 | file_put_contents(static::$connectionsFile, "--------------------------------------------------------------------- WORKERMAN CONNECTION STATUS --------------------------------------------------------------------------------\n", FILE_APPEND); |
2218 | file_put_contents(static::$connectionsFile, "PID Worker CID Trans Protocol ipv4 ipv6 Recv-Q Send-Q Bytes-R Bytes-W Status Local Address Foreign Address\n", FILE_APPEND); |
2219 | foreach (static::getAllWorkerPids() as $workerPid) { |
2220 | posix_kill($workerPid, SIGIO); |
2221 | } |
2222 | return; |
2223 | } |
2224 | |
2225 | // For child processes. |
2226 | $bytesFormat = function ($bytes) { |
2227 | if ($bytes > 1024 * 1024 * 1024 * 1024) { |
2228 | return round($bytes / (1024 * 1024 * 1024 * 1024), 1) . "TB"; |
2229 | } |
2230 | if ($bytes > 1024 * 1024 * 1024) { |
2231 | return round($bytes / (1024 * 1024 * 1024), 1) . "GB"; |
2232 | } |
2233 | if ($bytes > 1024 * 1024) { |
2234 | return round($bytes / (1024 * 1024), 1) . "MB"; |
2235 | } |
2236 | if ($bytes > 1024) { |
2237 | return round($bytes / (1024), 1) . "KB"; |
2238 | } |
2239 | return $bytes . "B"; |
2240 | }; |
2241 | |
2242 | $pid = posix_getpid(); |
2243 | $str = ''; |
2244 | reset(static::$workers); |
2245 | $currentWorker = current(static::$workers); |
2246 | $defaultWorkerName = $currentWorker->name; |
2247 | |
2248 | foreach (TcpConnection::$connections as $connection) { |
2249 | /** @var TcpConnection $connection */ |
2250 | $transport = $connection->transport; |
2251 | $ipv4 = $connection->isIpV4() ? ' 1' : ' 0'; |
2252 | $ipv6 = $connection->isIpV6() ? ' 1' : ' 0'; |
2253 | $recvQ = $bytesFormat($connection->getRecvBufferQueueSize()); |
2254 | $sendQ = $bytesFormat($connection->getSendBufferQueueSize()); |
2255 | $localAddress = trim($connection->getLocalAddress()); |
2256 | $remoteAddress = trim($connection->getRemoteAddress()); |
2257 | $state = $connection->getStatus(false); |
2258 | $bytesRead = $bytesFormat($connection->bytesRead); |
2259 | $bytesWritten = $bytesFormat($connection->bytesWritten); |
2260 | $id = $connection->id; |
2261 | $protocol = $connection->protocol ?: $connection->transport; |
2262 | $pos = strrpos($protocol, '\\'); |
2263 | if ($pos) { |
2264 | $protocol = substr($protocol, $pos + 1); |
2265 | } |
2266 | if (strlen($protocol) > 15) { |
2267 | $protocol = substr($protocol, 0, 13) . '..'; |
2268 | } |
2269 | $workerName = isset($connection->worker) ? $connection->worker->name : $defaultWorkerName; |
2270 | if (strlen($workerName) > 14) { |
2271 | $workerName = substr($workerName, 0, 12) . '..'; |
2272 | } |
2273 | $str .= str_pad((string)$pid, 9) . str_pad($workerName, 16) . str_pad((string)$id, 10) . str_pad($transport, 8) |
2274 | . str_pad($protocol, 16) . str_pad($ipv4, 7) . str_pad($ipv6, 7) . str_pad($recvQ, 13) |
2275 | . str_pad($sendQ, 13) . str_pad($bytesRead, 13) . str_pad($bytesWritten, 13) . ' ' |
2276 | . str_pad($state, 14) . ' ' . str_pad($localAddress, 22) . ' ' . str_pad($remoteAddress, 22) . "\n"; |
2277 | } |
2278 | if ($str) { |
2279 | file_put_contents(static::$connectionsFile, $str, FILE_APPEND); |
2280 | } |
2281 | } |
2282 | |
2283 | /** |
2284 | * Check errors when current process exited. |
2285 | * |
2286 | * @return void |
2287 | */ |
2288 | protected static function checkErrors(): void |
2289 | { |
2290 | if (static::STATUS_SHUTDOWN !== static::$status) { |
2291 | $errorMsg = DIRECTORY_SEPARATOR === '/' ? 'Worker[' . posix_getpid() . '] process terminated' : 'Worker process terminated'; |
2292 | $errors = error_get_last(); |
2293 | if ($errors && ($errors['type'] === E_ERROR || |
2294 | $errors['type'] === E_PARSE || |
2295 | $errors['type'] === E_CORE_ERROR || |
2296 | $errors['type'] === E_COMPILE_ERROR || |
2297 | $errors['type'] === E_RECOVERABLE_ERROR) |
2298 | ) { |
2299 | $errorMsg .= ' with ERROR: ' . static::getErrorType($errors['type']) . " \"{$errors['message']} in {$errors['file']} on line {$errors['line']}\""; |
2300 | } |
2301 | static::log($errorMsg); |
2302 | } |
2303 | } |
2304 | |
2305 | /** |
2306 | * Get error message by error code. |
2307 | * |
2308 | * @param int $type |
2309 | * @return string |
2310 | */ |
2311 | protected static function getErrorType(int $type): string |
2312 | { |
2313 | return self::ERROR_TYPE[$type] ?? ''; |
2314 | } |
2315 | |
2316 | /** |
2317 | * Log. |
2318 | * |
2319 | * @param Stringable|string $msg |
2320 | * @param bool $decorated |
2321 | * @return void |
2322 | */ |
2323 | public static function log(Stringable|string $msg, bool $decorated = false): void |
2324 | { |
2325 | $msg = trim((string)$msg); |
2326 | |
2327 | if (!static::$daemonize) { |
2328 | static::safeEcho("$msg\n", $decorated); |
2329 | } |
2330 | |
2331 | if (isset(static::$logFile)) { |
2332 | $pid = DIRECTORY_SEPARATOR === '/' ? posix_getpid() : 1; |
2333 | file_put_contents(static::$logFile, sprintf("%s pid:%d %s\n", date('Y-m-d H:i:s'), $pid, $msg), FILE_APPEND | LOCK_EX); |
2334 | } |
2335 | } |
2336 | |
2337 | /** |
2338 | * Safe Echo. |
2339 | * |
2340 | * @param string $msg |
2341 | * @param bool $decorated |
2342 | * @return void |
2343 | */ |
2344 | public static function safeEcho(string $msg, bool $decorated = false): void |
2345 | { |
2346 | if ((static::$outputDecorated ?? false) && $decorated) { |
2347 | $line = "\033[1A\n\033[K"; |
2348 | $white = "\033[47;30m"; |
2349 | $green = "\033[32;40m"; |
2350 | $end = "\033[0m"; |
2351 | } else { |
2352 | $line = ''; |
2353 | $white = ''; |
2354 | $green = ''; |
2355 | $end = ''; |
2356 | } |
2357 | |
2358 | $msg = str_replace(['<n>', '<w>', '<g>'], [$line, $white, $green], $msg); |
2359 | $msg = str_replace(['</n>', '</w>', '</g>'], $end, $msg); |
2360 | set_error_handler(static fn (): bool => true); |
2361 | if (!feof(self::$outputStream)) { |
2362 | fwrite(self::$outputStream, $msg); |
2363 | fflush(self::$outputStream); |
2364 | } |
2365 | restore_error_handler(); |
2366 | } |
2367 | |
2368 | /** |
2369 | * Listen. |
2370 | */ |
2371 | public function listen(): void |
2372 | { |
2373 | if (!$this->socketName) { |
2374 | return; |
2375 | } |
2376 | |
2377 | if (!$this->mainSocket) { |
2378 | |
2379 | $localSocket = $this->parseSocketAddress(); |
2380 | |
2381 | // Flag. |
2382 | $flags = $this->transport === 'udp' ? STREAM_SERVER_BIND : STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; |
2383 | $errNo = 0; |
2384 | $errMsg = ''; |
2385 | // SO_REUSEPORT. |
2386 | if ($this->reusePort) { |
2387 | stream_context_set_option($this->socketContext, 'socket', 'so_reuseport', 1); |
2388 | } |
2389 | |
2390 | // Create an Internet or Unix domain server socket. |
2391 | $this->mainSocket = stream_socket_server($localSocket, $errNo, $errMsg, $flags, $this->socketContext); |
2392 | if (!$this->mainSocket) { |
2393 | throw new RuntimeException($errMsg); |
2394 | } |
2395 | |
2396 | if ($this->transport === 'ssl') { |
2397 | stream_socket_enable_crypto($this->mainSocket, false); |
2398 | } elseif ($this->transport === 'unix') { |
2399 | $socketFile = substr($localSocket, 7); |
2400 | if ($this->user) { |
2401 | chown($socketFile, $this->user); |
2402 | } |
2403 | if ($this->group) { |
2404 | chgrp($socketFile, $this->group); |
2405 | } |
2406 | } |
2407 | |
2408 | // Try to open keepalive for tcp and disable Nagle algorithm. |
2409 | if (function_exists('socket_import_stream') && self::BUILD_IN_TRANSPORTS[$this->transport] === 'tcp') { |
2410 | set_error_handler(static fn (): bool => true); |
2411 | $socket = socket_import_stream($this->mainSocket); |
2412 | socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1); |
2413 | socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1); |
2414 | if (defined('TCP_KEEPIDLE') && defined('TCP_KEEPINTVL') && defined('TCP_KEEPCNT')) { |
2415 | socket_set_option($socket, SOL_TCP, TCP_KEEPIDLE, TcpConnection::TCP_KEEPALIVE_INTERVAL); |
2416 | socket_set_option($socket, SOL_TCP, TCP_KEEPINTVL, TcpConnection::TCP_KEEPALIVE_INTERVAL); |
2417 | socket_set_option($socket, SOL_TCP, TCP_KEEPCNT, 1); |
2418 | } |
2419 | restore_error_handler(); |
2420 | } |
2421 | |
2422 | // Non blocking. |
2423 | stream_set_blocking($this->mainSocket, false); |
2424 | } |
2425 | |
2426 | $this->resumeAccept(); |
2427 | } |
2428 | |
2429 | /** |
2430 | * Unlisten. |
2431 | * |
2432 | * @return void |
2433 | */ |
2434 | public function unlisten(): void |
2435 | { |
2436 | $this->pauseAccept(); |
2437 | if ($this->mainSocket) { |
2438 | set_error_handler(static fn (): bool => true); |
2439 | fclose($this->mainSocket); |
2440 | restore_error_handler(); |
2441 | $this->mainSocket = null; |
2442 | } |
2443 | } |
2444 | |
2445 | /** |
2446 | * Check port available. |
2447 | * |
2448 | * @return void |
2449 | */ |
2450 | protected static function checkPortAvailable(): void |
2451 | { |
2452 | foreach (static::$workers as $worker) { |
2453 | $socketName = $worker->getSocketName(); |
2454 | if (DIRECTORY_SEPARATOR === '/' // if linux |
2455 | && static::$status === static::STATUS_STARTING // only for starting status |
2456 | && $worker->transport === 'tcp' // if tcp socket |
2457 | && !str_starts_with($socketName, 'unix') // if not unix socket |
2458 | && !str_starts_with($socketName, 'udp')) { // if not udp socket |
2459 | |
2460 | $address = parse_url($socketName); |
2461 | if (isset($address['host']) && isset($address['port'])) { |
2462 | $address = "tcp://{$address['host']}:{$address['port']}"; |
2463 | $server = null; |
2464 | set_error_handler(function ($code, $msg) { |
2465 | throw new RuntimeException($msg); |
2466 | }); |
2467 | $server = stream_socket_server($address, $code, $msg); |
2468 | if ($server) { |
2469 | fclose($server); |
2470 | } |
2471 | restore_error_handler(); |
2472 | } |
2473 | } |
2474 | } |
2475 | } |
2476 | |
2477 | /** |
2478 | * Parse local socket address. |
2479 | */ |
2480 | protected function parseSocketAddress(): ?string |
2481 | { |
2482 | if (!$this->socketName) { |
2483 | return null; |
2484 | } |
2485 | // Get the application layer communication protocol and listening address. |
2486 | [$scheme, $address] = explode(':', $this->socketName, 2); |
2487 | // Check application layer protocol class. |
2488 | if (!isset(self::BUILD_IN_TRANSPORTS[$scheme])) { |
2489 | $scheme = ucfirst($scheme); |
2490 | $this->protocol = $scheme[0] === '\\' ? $scheme : 'Protocols\\' . $scheme; |
2491 | if (!class_exists($this->protocol)) { |
2492 | $this->protocol = "Workerman\\Protocols\\$scheme"; |
2493 | if (!class_exists($this->protocol)) { |
2494 | throw new RuntimeException("class \\Protocols\\$scheme not exist"); |
2495 | } |
2496 | } |
2497 | |
2498 | if (!isset(self::BUILD_IN_TRANSPORTS[$this->transport])) { |
2499 | throw new RuntimeException('Bad worker->transport ' . var_export($this->transport, true)); |
2500 | } |
2501 | } else if ($this->transport === 'tcp') { |
2502 | $this->transport = $scheme; |
2503 | } |
2504 | //local socket |
2505 | return self::BUILD_IN_TRANSPORTS[$this->transport] . ":" . $address; |
2506 | } |
2507 | |
2508 | /** |
2509 | * Pause accept new connections. |
2510 | * |
2511 | * @return void |
2512 | */ |
2513 | public function pauseAccept(): void |
2514 | { |
2515 | if (static::$globalEvent !== null && $this->pauseAccept === false && $this->mainSocket !== null) { |
2516 | static::$globalEvent->offReadable($this->mainSocket); |
2517 | $this->pauseAccept = true; |
2518 | } |
2519 | } |
2520 | |
2521 | /** |
2522 | * Resume accept new connections. |
2523 | * |
2524 | * @return void |
2525 | */ |
2526 | public function resumeAccept(): void |
2527 | { |
2528 | // Register a listener to be notified when server socket is ready to read. |
2529 | if (static::$globalEvent !== null && $this->pauseAccept === true && $this->mainSocket !== null) { |
2530 | if ($this->transport !== 'udp') { |
2531 | static::$globalEvent->onReadable($this->mainSocket, $this->acceptTcpConnection(...)); |
2532 | } else { |
2533 | static::$globalEvent->onReadable($this->mainSocket, $this->acceptUdpConnection(...)); |
2534 | } |
2535 | $this->pauseAccept = false; |
2536 | } |
2537 | } |
2538 | |
2539 | /** |
2540 | * Get socket name. |
2541 | * |
2542 | * @return string |
2543 | */ |
2544 | public function getSocketName(): string |
2545 | { |
2546 | return $this->socketName ? lcfirst($this->socketName) : 'none'; |
2547 | } |
2548 | |
2549 | /** |
2550 | * Run worker instance. |
2551 | * |
2552 | * @return void |
2553 | * @throws Throwable |
2554 | */ |
2555 | public function run(): void |
2556 | { |
2557 | $this->listen(); |
2558 | |
2559 | if (!$this->onWorkerStart) { |
2560 | return; |
2561 | } |
2562 | |
2563 | // Try to emit onWorkerStart callback. |
2564 | $callback = function() { |
2565 | try { |
2566 | ($this->onWorkerStart)($this); |
2567 | } catch (Throwable $e) { |
2568 | // Avoid rapid infinite loop exit. |
2569 | sleep(1); |
2570 | static::stopAll(250, $e); |
2571 | } |
2572 | }; |
2573 | |
2574 | switch (Worker::$eventLoopClass) { |
2575 | case Swoole::class: |
2576 | case Swow::class: |
2577 | case Fiber::class: |
2578 | Coroutine::create($callback); |
2579 | break; |
2580 | default: |
2581 | (new \Fiber($callback))->start(); |
2582 | } |
2583 | } |
2584 | |
2585 | /** |
2586 | * Stop current worker instance. |
2587 | * |
2588 | * @param bool $force |
2589 | * @return void |
2590 | */ |
2591 | public function stop(bool $force = true): void |
2592 | { |
2593 | if ($this->stopping === true) { |
2594 | return; |
2595 | } |
2596 | // Try to emit onWorkerStop callback. |
2597 | if ($this->onWorkerStop) { |
2598 | try { |
2599 | ($this->onWorkerStop)($this); |
2600 | } catch (Throwable $e) { |
2601 | static::log($e); |
2602 | } |
2603 | } |
2604 | // Remove listener for server socket. |
2605 | $this->unlisten(); |
2606 | // Close all connections for the worker. |
2607 | if (!static::getGracefulStop()) { |
2608 | foreach ($this->connections as $connection) { |
2609 | if ($force || !$connection->getRecvBufferQueueSize()) { |
2610 | $connection->close(); |
2611 | } |
2612 | } |
2613 | } |
2614 | // Clear callback. |
2615 | $this->onMessage = $this->onClose = $this->onError = $this->onBufferDrain = $this->onBufferFull = null; |
2616 | $this->stopping = true; |
2617 | } |
2618 | |
2619 | /** |
2620 | * Accept a connection. |
2621 | * |
2622 | * @param resource $socket |
2623 | * @return void |
2624 | */ |
2625 | protected function acceptTcpConnection(mixed $socket): void |
2626 | { |
2627 | // Accept a connection on server socket. |
2628 | set_error_handler(static fn (): bool => true); |
2629 | $newSocket = stream_socket_accept($socket, 0, $remoteAddress); |
2630 | restore_error_handler(); |
2631 | |
2632 | // Thundering herd. |
2633 | if (!$newSocket) { |
2634 | return; |
2635 | } |
2636 | |
2637 | // TcpConnection. |
2638 | $connection = new TcpConnection(static::$globalEvent, $newSocket, $remoteAddress); |
2639 | $this->connections[$connection->id] = $connection; |
2640 | $connection->worker = $this; |
2641 | $connection->protocol = $this->protocol; |
2642 | $connection->transport = $this->transport; |
2643 | $connection->onMessage = $this->onMessage; |
2644 | $connection->onClose = $this->onClose; |
2645 | $connection->onError = $this->onError; |
2646 | $connection->onBufferDrain = $this->onBufferDrain; |
2647 | $connection->onBufferFull = $this->onBufferFull; |
2648 | |
2649 | // Try to emit onConnect callback. |
2650 | if ($this->onConnect) { |
2651 | try { |
2652 | ($this->onConnect)($connection); |
2653 | } catch (Throwable $e) { |
2654 | static::stopAll(250, $e); |
2655 | } |
2656 | } |
2657 | } |
2658 | |
2659 | /** |
2660 | * For udp package. |
2661 | * |
2662 | * @param resource $socket |
2663 | * @return void |
2664 | */ |
2665 | protected function acceptUdpConnection(mixed $socket): void |
2666 | { |
2667 | set_error_handler(static fn (): bool => true); |
2668 | $recvBuffer = stream_socket_recvfrom($socket, UdpConnection::MAX_UDP_PACKAGE_SIZE, 0, $remoteAddress); |
2669 | restore_error_handler(); |
2670 | if (false === $recvBuffer || empty($remoteAddress)) { |
2671 | return; |
2672 | } |
2673 | // UdpConnection. |
2674 | $connection = new UdpConnection($socket, $remoteAddress); |
2675 | $connection->protocol = $this->protocol; |
2676 | $messageCallback = $this->onMessage; |
2677 | if ($messageCallback) { |
2678 | try { |
2679 | if ($this->protocol !== null) { |
2680 | $parser = $this->protocol; |
2681 | if ($parser && method_exists($parser, 'input')) { |
2682 | while ($recvBuffer !== '') { |
2683 | $len = $parser::input($recvBuffer, $connection); |
2684 | if ($len === 0) { |
2685 | return; |
2686 | } |
2687 | $package = substr($recvBuffer, 0, $len); |
2688 | $recvBuffer = substr($recvBuffer, $len); |
2689 | $data = $parser::decode($package, $connection); |
2690 | if ($data === false) { |
2691 | continue; |
2692 | } |
2693 | $messageCallback($connection, $data); |
2694 | } |
2695 | } else { |
2696 | $data = $parser::decode($recvBuffer, $connection); |
2697 | // Discard bad packets. |
2698 | if ($data === false) { |
2699 | return; |
2700 | } |
2701 | $messageCallback($connection, $data); |
2702 | } |
2703 | } else { |
2704 | $messageCallback($connection, $recvBuffer); |
2705 | } |
2706 | ConnectionInterface::$statistics['total_request']++; |
2707 | } catch (Throwable $e) { |
2708 | static::stopAll(250, $e); |
2709 | } |
2710 | } |
2711 | } |
2712 | |
2713 | /** |
2714 | * Check master process is alive |
2715 | * |
2716 | * @param int $masterPid |
2717 | * @return bool |
2718 | */ |
2719 | protected static function checkMasterIsAlive(int $masterPid): bool |
2720 | { |
2721 | if (empty($masterPid)) { |
2722 | return false; |
2723 | } |
2724 | |
2725 | $masterIsAlive = posix_kill($masterPid, 0) && posix_getpid() !== $masterPid; |
2726 | if (!$masterIsAlive) { |
2727 | static::log("Master pid:$masterPid is not alive"); |
2728 | return false; |
2729 | } |
2730 | |
2731 | $cmdline = "/proc/$masterPid/cmdline"; |
2732 | if (!is_readable($cmdline)) { |
2733 | return true; |
2734 | } |
2735 | |
2736 | $content = file_get_contents($cmdline); |
2737 | if (empty($content)) { |
2738 | return true; |
2739 | } |
2740 | |
2741 | return str_contains($content, 'WorkerMan') || str_contains($content, 'php'); |
2742 | } |
2743 | |
2744 | /** |
2745 | * If worker is running. |
2746 | * |
2747 | * @return bool |
2748 | */ |
2749 | public static function isRunning(): bool |
2750 | { |
2751 | return Worker::$status !== Worker::STATUS_INITIAL; |
2752 | } |
2753 | } |