| Code Coverage
| Lines | Functions and Methods | Classes and Traits
Total | 24.31% covered (danger) | 24.31% | 35 / 144 | 9.52% covered (danger) | 9.52% | 2 / 21 | CRAP | 0.00% covered (danger) | 0.00% | 0 / 1
Select | 24.31% covered (danger) | 24.31% | 35 / 144 | 9.52% covered (danger) | 9.52% | 2 / 21 | 1784.37 | 0.00% covered (danger) | 0.00% | 0 / 1
__construct | 100.00% covered (success) | 100.00% | 2 / 2 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
delay | 0.00% covered (danger) | 0.00% | 0 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
repeat | 0.00% covered (danger) | 0.00% | 0 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
offDelay | 0.00% covered (danger) | 0.00% | 0 / 4 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
offRepeat | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
onReadable | 75.00% covered (warning) | 75.00% | 6 / 8 | 0.00% covered (danger) | 0.00% | 0 / 1 | 4.25 |
offReadable | 80.00% covered (warning) | 80.00% | 4 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.03 |
onWritable | 0.00% covered (danger) | 0.00% | 0 / 8 | 0.00% covered (danger) | 0.00% | 0 / 1 | 20 |
offWritable | 60.00% covered (warning) | 60.00% | 3 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.26 |
onExcept | 0.00% covered (danger) | 0.00% | 0 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
offExcept | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
onSignal | 75.00% covered (warning) | 75.00% | 3 / 4 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2.06 |
offSignal | 0.00% covered (danger) | 0.00% | 0 / 7 | 0.00% covered (danger) | 0.00% | 0 / 1 | 12 |
tick | 0.00% covered (danger) | 0.00% | 0 / 26 | 0.00% covered (danger) | 0.00% | 0 / 1 | 56 |
setNextTickTime | 0.00% covered (danger) | 0.00% | 0 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
deleteAllTimer | 0.00% covered (danger) | 0.00% | 0 / 3 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
run | 57.69% covered (warning) | 57.69% | 15 / 26 | 0.00% covered (danger) | 0.00% | 0 / 1 | 35.39 |
stop | 0.00% covered (danger) | 0.00% | 0 / 11 | 0.00% covered (danger) | 0.00% | 0 / 1 | 6 |
getTimerCount | 0.00% covered (danger) | 0.00% | 0 / 1 | 0.00% covered (danger) | 0.00% | 0 / 1 | 2 |
setErrorHandler | 100.00% covered (success) | 100.00% | 1 / 1 | 100.00% covered (success) | 100.00% | 1 / 1 | 1 |
safeCall | 20.00% covered (danger) | 20.00% | 1 / 5 | 0.00% covered (danger) | 0.00% | 0 / 1 | 7.61 |

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\Events;
18 |
19 | use SplPriorityQueue;
20 | use Throwable;
21 | use function count;
22 | use function max;
23 | use function microtime;
24 | use function pcntl_signal;
25 | use function pcntl_signal_dispatch;
26 | use const DIRECTORY_SEPARATOR;
27 |
28 | /**
29 | * select eventloop
30 | */
31 | final class Select implements EventInterface
32 | {
33 | /**
34 | * Running.
35 | *
36 | * @var bool
37 | */
38 | private bool $running = true;
39 |
40 | /**
41 | * All listeners for read/write event.
42 | *
43 | * @var array<int, callable>
44 | */
45 | private array $readEvents = [];
46 |
47 | /**
48 | * All listeners for read/write event.
49 | *
50 | * @var array<int, callable>
51 | */
52 | private array $writeEvents = [];
53 |
54 | /**
55 | * @var array<int, callable>
56 | */
57 | private array $exceptEvents = [];
58 |
59 | /**
60 | * Event listeners of signal.
61 | *
62 | * @var array<int, callable>
63 | */
64 | private array $signalEvents = [];
65 |
66 | /**
67 | * Fds waiting for read event.
68 | *
69 | * @var array<int, resource>
70 | */
71 | private array $readFds = [];
72 |
73 | /**
74 | * Fds waiting for write event.
75 | *
76 | * @var array<int, resource>
77 | */
78 | private array $writeFds = [];
79 |
80 | /**
81 | * Fds waiting for except event.
82 | *
83 | * @var array<int, resource>
84 | */
85 | private array $exceptFds = [];
86 |
87 | /**
88 | * Timer scheduler.
89 | * {['data':timer_id, 'priority':run_timestamp], ..}
90 | *
91 | * @var SplPriorityQueue
92 | */
93 | private SplPriorityQueue $scheduler;
94 |
95 | /**
96 | * All timer event listeners.
97 | * [[func, args, flag, timer_interval], ..]
98 | *
99 | * @var array
100 | */
101 | private array $eventTimer = [];
102 |
103 | /**
104 | * Timer id.
105 | *
106 | * @var int
107 | */
108 | private int $timerId = 1;
109 |
110 | /**
111 | * Select timeout.
112 | *
113 | * @var int
114 | */
115 | private int $selectTimeout = self::MAX_SELECT_TIMOUT_US;
116 |
117 | /**
118 | * Next run time of the timer.
119 | *
120 | * @var float
121 | */
122 | private float $nextTickTime = 0;
123 |
124 | /**
125 | * @var ?callable
126 | */
127 | private $errorHandler = null;
128 |
129 | /**
130 | * Select timeout.
131 | *
132 | * @var int
133 | */
134 | const MAX_SELECT_TIMOUT_US = 800000;
135 |
136 | /**
137 | * Construct.
138 | */
139 | public function __construct()
140 | {
141 | $this->scheduler = new SplPriorityQueue();
142 | $this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
143 | }
144 |
145 | /**
146 | * {@inheritdoc}
147 | */
148 | public function delay(float $delay, callable $func, array $args = []): int
149 | {
150 | $timerId = $this->timerId++;
151 | $runTime = microtime(true) + $delay;
152 | $this->scheduler->insert($timerId, -$runTime);
153 | $this->eventTimer[$timerId] = [$func, $args];
154 | if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
155 | $this->setNextTickTime($runTime);
156 | }
157 | return $timerId;
158 | }
159 |
160 | /**
161 | * {@inheritdoc}
162 | */
163 | public function repeat(float $interval, callable $func, array $args = []): int
164 | {
165 | $timerId = $this->timerId++;
166 | $runTime = microtime(true) + $interval;
167 | $this->scheduler->insert($timerId, -$runTime);
168 | $this->eventTimer[$timerId] = [$func, $args, $interval];
169 | if ($this->nextTickTime == 0 || $this->nextTickTime > $runTime) {
170 | $this->setNextTickTime($runTime);
171 | }
172 | return $timerId;
173 | }
174 |
175 | /**
176 | * {@inheritdoc}
177 | */
178 | public function offDelay(int $timerId): bool
179 | {
180 | if (isset($this->eventTimer[$timerId])) {
181 | unset($this->eventTimer[$timerId]);
182 | return true;
183 | }
184 | return false;
185 | }
186 |
187 | /**
188 | * {@inheritdoc}
189 | */
190 | public function offRepeat(int $timerId): bool
191 | {
192 | return $this->offDelay($timerId);
193 | }
194 |
195 | /**
196 | * {@inheritdoc}
197 | */
198 | public function onReadable($stream, callable $func): void
199 | {
200 | $count = count($this->readFds);
201 | if ($count >= 1024) {
202 | trigger_error("System call select exceeded the maximum number of connections 1024, please install event extension for more connections.", E_USER_WARNING);
203 | } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
204 | trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
205 | }
206 | $fdKey = (int)$stream;
207 | $this->readEvents[$fdKey] = $func;
208 | $this->readFds[$fdKey] = $stream;
209 | }
210 |
211 | /**
212 | * {@inheritdoc}
213 | */
214 | public function offReadable($stream): bool
215 | {
216 | $fdKey = (int)$stream;
217 | if (isset($this->readEvents[$fdKey])) {
218 | unset($this->readEvents[$fdKey], $this->readFds[$fdKey]);
219 | return true;
220 | }
221 | return false;
222 | }
223 |
224 | /**
225 | * {@inheritdoc}
226 | */
227 | public function onWritable($stream, callable $func): void
228 | {
229 | $count = count($this->writeFds);
230 | if ($count >= 1024) {
231 | trigger_error("System call select exceeded the maximum number of connections 1024, please install event/libevent extension for more connections.", E_USER_WARNING);
232 | } else if (DIRECTORY_SEPARATOR !== '/' && $count >= 256) {
233 | trigger_error("System call select exceeded the maximum number of connections 256.", E_USER_WARNING);
234 | }
235 | $fdKey = (int)$stream;
236 | $this->writeEvents[$fdKey] = $func;
237 | $this->writeFds[$fdKey] = $stream;
238 | }
239 |
240 | /**
241 | * {@inheritdoc}
242 | */
243 | public function offWritable($stream): bool
244 | {
245 | $fdKey = (int)$stream;
246 | if (isset($this->writeEvents[$fdKey])) {
247 | unset($this->writeEvents[$fdKey], $this->writeFds[$fdKey]);
248 | return true;
249 | }
250 | return false;
251 | }
252 |
253 | /**
254 | * On except.
255 | *
256 | * @param resource $stream
257 | * @param callable $func
258 | */
259 | public function onExcept($stream, callable $func): void
260 | {
261 | $fdKey = (int)$stream;
262 | $this->exceptEvents[$fdKey] = $func;
263 | $this->exceptFds[$fdKey] = $stream;
264 | }
265 |
266 | /**
267 | * Off except.
268 | *
269 | * @param resource $stream
270 | * @return bool
271 | */
272 | public function offExcept($stream): bool
273 | {
274 | $fdKey = (int)$stream;
275 | if (isset($this->exceptEvents[$fdKey])) {
276 | unset($this->exceptEvents[$fdKey], $this->exceptFds[$fdKey]);
277 | return true;
278 | }
279 | return false;
280 | }
281 |
282 | /**
283 | * {@inheritdoc}
284 | */
285 | public function onSignal(int $signal, callable $func): void
286 | {
287 | if (!function_exists('pcntl_signal')) {
288 | return;
289 | }
290 | $this->signalEvents[$signal] = $func;
291 | pcntl_signal($signal, fn () => $this->safeCall($this->signalEvents[$signal], [$signal]));
292 | }
293 |
294 | /**
295 | * {@inheritdoc}
296 | */
297 | public function offSignal(int $signal): bool
298 | {
299 | if (!function_exists('pcntl_signal')) {
300 | return false;
301 | }
302 | pcntl_signal($signal, SIG_IGN);
303 | if (isset($this->signalEvents[$signal])) {
304 | unset($this->signalEvents[$signal]);
305 | return true;
306 | }
307 | return false;
308 | }
309 |
310 | /**
311 | * Tick for timer.
312 | *
313 | * @return void
314 | */
315 | protected function tick(): void
316 | {
317 | $tasksToInsert = [];
318 | while (!$this->scheduler->isEmpty()) {
319 | $schedulerData = $this->scheduler->top();
320 | $timerId = $schedulerData['data'];
321 | $nextRunTime = -$schedulerData['priority'];
322 | $timeNow = microtime(true);
323 | $this->selectTimeout = (int)(($nextRunTime - $timeNow) * 1000000);
324 |
325 | if ($this->selectTimeout <= 0) {
326 | $this->scheduler->extract();
327 |
328 | if (!isset($this->eventTimer[$timerId])) {
329 | continue;
330 | }
331 |
332 | // [func, args, timer_interval]
333 | $taskData = $this->eventTimer[$timerId];
334 | if (isset($taskData[2])) {
335 | $nextRunTime = $timeNow + $taskData[2];
336 | $tasksToInsert[] = [$timerId, -$nextRunTime];
337 | } else {
338 | unset($this->eventTimer[$timerId]);
339 | }
340 | $this->safeCall($taskData[0], $taskData[1]);
341 | } else {
342 | break;
343 | }
344 | }
345 | foreach ($tasksToInsert as $item) {
346 | $this->scheduler->insert($item[0], $item[1]);
347 | }
348 | if (!$this->scheduler->isEmpty()) {
349 | $schedulerData = $this->scheduler->top();
350 | $nextRunTime = -$schedulerData['priority'];
351 | $this->setNextTickTime($nextRunTime);
352 | return;
353 | }
354 | $this->setNextTickTime(0);
355 | }
356 |
357 | /**
358 | * Set next tick time.
359 | *
360 | * @param float $nextTickTime
361 | * @return void
362 | */
363 | protected function setNextTickTime(float $nextTickTime): void
364 | {
365 | $this->nextTickTime = $nextTickTime;
366 | if ($nextTickTime == 0) {
367 | // Swow will affect the signal interruption characteristics of stream_select,
368 | // so a shorter timeout should be used to detect signals.
369 | $this->selectTimeout = self::MAX_SELECT_TIMOUT_US;
370 | return;
371 | }
372 | $this->selectTimeout = min(max((int)(($nextTickTime - microtime(true)) * 1000000), 0), self::MAX_SELECT_TIMOUT_US);
373 | }
374 |
375 | /**
376 | * {@inheritdoc}
377 | */
378 | public function deleteAllTimer(): void
379 | {
380 | $this->scheduler = new SplPriorityQueue();
381 | $this->scheduler->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
382 | $this->eventTimer = [];
383 | }
384 |
385 | /**
386 | * {@inheritdoc}
387 | */
388 | public function run(): void
389 | {
390 | while ($this->running) {
391 | $read = $this->readFds;
392 | $write = $this->writeFds;
393 | $except = $this->exceptFds;
394 | if ($read || $write || $except) {
395 | // Waiting read/write/signal/timeout events.
396 | try {
397 | @stream_select($read, $write, $except, 0, $this->selectTimeout);
398 | } catch (Throwable) {
399 | // do nothing
400 | }
401 | } else {
402 | $this->selectTimeout >= 1 && usleep($this->selectTimeout);
403 | }
404 |
405 | foreach ($read as $fd) {
406 | $fdKey = (int)$fd;
407 | if (isset($this->readEvents[$fdKey])) {
408 | $this->readEvents[$fdKey]($fd);
409 | }
410 | }
411 |
412 | foreach ($write as $fd) {
413 | $fdKey = (int)$fd;
414 | if (isset($this->writeEvents[$fdKey])) {
415 | $this->writeEvents[$fdKey]($fd);
416 | }
417 | }
418 |
419 | foreach ($except as $fd) {
420 | $fdKey = (int)$fd;
421 | if (isset($this->exceptEvents[$fdKey])) {
422 | $this->exceptEvents[$fdKey]($fd);
423 | }
424 | }
425 |
426 | if ($this->nextTickTime > 0) {
427 | if (microtime(true) >= $this->nextTickTime) {
428 | $this->tick();
429 | } else {
430 | $this->selectTimeout = (int)(($this->nextTickTime - microtime(true)) * 1000000);
431 | }
432 | }
433 |
434 | // The $this->signalEvents are empty under Windows, make sure not to call pcntl_signal_dispatch.
435 | if ($this->signalEvents) {
436 | // Calls signal handlers for pending signals
437 | pcntl_signal_dispatch();
438 | }
439 | }
440 | }
441 |
442 | /**
443 | * {@inheritdoc}
444 | */
445 | public function stop(): void
446 | {
447 | $this->running = false;
448 | $this->deleteAllTimer();
449 | foreach ($this->signalEvents as $signal => $item) {
450 | $this->offsignal($signal);
451 | }
452 | $this->readFds = [];
453 | $this->writeFds = [];
454 | $this->exceptFds = [];
455 | $this->readEvents = [];
456 | $this->writeEvents = [];
457 | $this->exceptEvents = [];
458 | $this->signalEvents = [];
459 | }
460 |
461 | /**
462 | * {@inheritdoc}
463 | */
464 | public function getTimerCount(): int
465 | {
466 | return count($this->eventTimer);
467 | }
468 |
469 | /**
470 | * {@inheritdoc}
471 | */
472 | public function setErrorHandler(callable $errorHandler): void
473 | {
474 | $this->errorHandler = $errorHandler;
475 | }
476 |
477 | /**
478 | * @param callable $func
479 | * @param array $args
480 | * @return void
481 | */
482 | private function safeCall(callable $func, array $args = []): void
483 | {
484 | try {
485 | $func(...$args);
486 | } catch (Throwable $e) {
487 | if ($this->errorHandler === null) {
488 | echo $e;
489 | } else {
490 | ($this->errorHandler)($e);
491 | }
492 | }
493 | }
494 | }