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
15declare(strict_types=1);
16
17namespace Workerman\Events;
18
19use SplPriorityQueue;
20use Throwable;
21use function count;
22use function max;
23use function microtime;
24use function pcntl_signal;
25use function pcntl_signal_dispatch;
26use const DIRECTORY_SEPARATOR;
27
28/**
29 * select eventloop
30 */
31final 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}