| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 37.28% covered (danger) | 37.28% | 390 / 1046 | 17.39% covered (danger) | 17.39% | 12 / 69 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
Worker | 37.28% covered (danger) | 37.28% | 390 / 1046 | 17.39% covered (danger) | 17.39% | 12 / 69 | 46680.93 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 90.00% covered (success) | 90.00% | 9 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.00 |
runAll | 76.47% covered (warning) | 76.47% | 13 / 17 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.05 |
checkSapiEnv | 88.10% covered (warning) | 88.10% | 37 / 42 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6.06 |
initStdOut | 57.14% covered (warning) | 57.14% | 4 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6.97 |
hasColorSupport | 40.00% covered (danger) | 40.00% | 4 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 21.82 |
init | 78.12% covered (warning) | 78.12% | 25 / 32 | 0.00% covered (danger) | 0.00% | 0 / 1 | 13.51 |
initGlobalEvent | 50.00% covered (danger) | 50.00% | 6 / 12 | 0.00% covered (danger) | 0.00% | 0 / 1 | 10.50 |
lock | 92.31% covered (success) | 92.31% | 12 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6.02 |
initWorkers | 80.95% covered (warning) | 80.95% | 17 / 21 | 0.00% covered (danger) | 0.00% | 0 / 1 | 13.00 |
getAllWorkers | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
getEventLoop | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
getMainSocket | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
initId | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 3 |
getCurrentUser | 100.00% covered (success) | 100.00% | 2 / 2 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
displayUI | 78.38% covered (warning) | 78.38% | 29 / 37 | 0.00% covered (danger) | 0.00% | 0 / 1 | 15.98 |
getVersionLine | 100.00% covered (success) | 100.00% | 5 / 5 | 100.00% covered (success) | 100.00% | 1 / 1 | 3 |
getUiColumns | 100.00% covered (success) | 100.00% | 9 / 9 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
getSingleLineTotalLength | 100.00% covered (success) | 100.00% | 7 / 7 | 100.00% covered (success) | 100.00% | 1 / 1 | 4 |
parseCommand | 34.04% covered (danger) | 34.04% | 32 / 94 | 0.00% covered (danger) | 0.00% | 0 / 1 | 429.82 |
getArgv | 100.00% covered (success) | 100.00% | 2 / 2 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
formatProcessStatusData | 0.00% covered (danger) | 0.00% | 0 / 65 | 0.00% covered (danger) | 0.00% | 0 / 1 | 182 |
formatConnectionStatusData | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
installSignal | 83.33% covered (warning) | 83.33% | 5 / 6 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.04 |
reinstallSignal | 80.00% covered (warning) | 80.00% | 4 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.07 |
signalHandler | 12.00% covered (danger) | 12.00% | 3 / 25 | 0.00% covered (danger) | 0.00% | 0 / 1 | 110.13 |
getSignalName | 20.00% covered (danger) | 20.00% | 2 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 72.95 |
daemonize | 13.33% covered (danger) | 13.33% | 2 / 15 | 0.00% covered (danger) | 0.00% | 0 / 1 | 49.66 |
resetStd | 11.11% covered (danger) | 11.11% | 2 / 18 | 0.00% covered (danger) | 0.00% | 0 / 1 | 65.89 |
saveMasterPid | 60.00% covered (warning) | 60.00% | 3 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 3.58 |
getAllWorkerPids | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
forkWorkers | 66.67% covered (warning) | 66.67% | 2 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.15 |
forkWorkersForLinux | 83.33% covered (warning) | 83.33% | 5 / 6 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.12 |
forkWorkersForWindows | 0.00% covered (danger) | 0.00% | 0 / 38 | 0.00% covered (danger) | 0.00% | 0 / 1 | 90 |
getStartFilesForWindows | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
forkOneWorkerForWindows | 0.00% covered (danger) | 0.00% | 0 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
checkWorkerStatusForWindows | 0.00% covered (danger) | 0.00% | 0 / 8 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
forkOneWorkerForLinux | 71.79% covered (warning) | 71.79% | 28 / 39 | 0.00% covered (danger) | 0.00% | 0 / 1 | 10.82 |
getId | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
setUserAndGroup | 40.00% covered (danger) | 40.00% | 6 / 15 | 0.00% covered (danger) | 0.00% | 0 / 1 | 26.50 |
setProcessTitle | 100.00% covered (success) | 100.00% | 3 / 3 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
monitorWorkers | 0.00% covered (danger) | 0.00% | 0 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
monitorWorkersForLinux | 0.00% covered (danger) | 0.00% | 0 / 31 | 0.00% covered (danger) | 0.00% | 0 / 1 | 210 |
monitorWorkersForWindows | 0.00% covered (danger) | 0.00% | 0 / 2 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
exitAndClearAll | 0.00% covered (danger) | 0.00% | 0 / 14 | 0.00% covered (danger) | 0.00% | 0 / 1 | 56 |
reload | 0.00% covered (danger) | 0.00% | 0 / 36 | 0.00% covered (danger) | 0.00% | 0 / 1 | 210 |
stopAll | 15.62% covered (danger) | 15.62% | 5 / 32 | 0.00% covered (danger) | 0.00% | 0 / 1 | 285.90 |
checkIfChildRunning | 0.00% covered (danger) | 0.00% | 0 / 4 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20 |
getStatus | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
getGracefulStop | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
writeStatisticsToStatusFile | 0.00% covered (danger) | 0.00% | 0 / 58 | 0.00% covered (danger) | 0.00% | 0 / 1 | 132 |
getUiColumnLength | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
writeConnectionsStatisticsToStatusFile | 0.00% covered (danger) | 0.00% | 0 / 49 | 0.00% covered (danger) | 0.00% | 0 / 1 | 272 |
checkErrors | 0.00% covered (danger) | 0.00% | 0 / 10 | 0.00% covered (danger) | 0.00% | 0 / 1 | 90 |
getErrorType | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
log | 100.00% covered (success) | 100.00% | 6 / 6 | 100.00% covered (success) | 100.00% | 1 / 1 | 4 |
safeEcho | 75.00% covered (warning) | 75.00% | 12 / 16 | 0.00% covered (danger) | 0.00% | 0 / 1 | 4.25 |
listen | 71.88% covered (warning) | 71.88% | 23 / 32 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20.01 |
unlisten | 0.00% covered (danger) | 0.00% | 0 / 6 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
checkPortAvailable | 94.12% covered (success) | 94.12% | 16 / 17 | 0.00% covered (danger) | 0.00% | 0 / 1 | 10.02 |
parseSocketAddress | 66.67% covered (warning) | 66.67% | 10 / 15 | 0.00% covered (danger) | 0.00% | 0 / 1 | 10.37 |
pauseAccept | 33.33% covered (danger) | 33.33% | 1 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 8.74 |
resumeAccept | 80.00% covered (warning) | 80.00% | 4 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 5.20 |
getSocketName | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 2 |
run | 23.08% covered (danger) | 23.08% | 3 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 29.30 |
stop | 23.08% covered (danger) | 23.08% | 3 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 37.13 |
acceptTcpConnection | 89.47% covered (warning) | 89.47% | 17 / 19 | 0.00% covered (danger) | 0.00% | 0 / 1 | 4.02 |
acceptUdpConnection | 0.00% covered (danger) | 0.00% | 0 / 30 | 0.00% covered (danger) | 0.00% | 0 / 1 | 156 |
checkMasterIsAlive | 15.38% covered (danger) | 15.38% | 2 / 13 | 0.00% covered (danger) | 0.00% | 0 / 1 | 36.69 |
isRunning | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 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 | }