diff --git a/.travis.yml b/.travis.yml index 37ba1989..0340c258 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ before_script: cd libevent-0.0.5 && phpize && ./configure && make && sudo make install; echo "extension=libevent.so" >> `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"`; fi" + - echo "yes" | pecl install event - composer self-update - composer install --dev --prefer-source diff --git a/src/React/EventLoop/ExtEventLoop.php b/src/React/EventLoop/ExtEventLoop.php new file mode 100644 index 00000000..afa1d98a --- /dev/null +++ b/src/React/EventLoop/ExtEventLoop.php @@ -0,0 +1,308 @@ +eventBase = new EventBase(); + $this->nextTickQueue = new NextTickQueue($this); + $this->timerEvents = new SplObjectStorage(); + + $this->createTimerCallback(); + $this->createStreamCallback(); + } + + /** + * {@inheritdoc} + */ + public function addReadStream($stream, callable $listener) + { + $key = (int) $stream; + + if (!isset($this->readListeners[$key])) { + $this->readListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, Event::READ); + } + } + + /** + * {@inheritdoc} + */ + public function addWriteStream($stream, callable $listener) + { + $key = (int) $stream; + + if (!isset($this->writeListeners[$key])) { + $this->writeListeners[$key] = $listener; + $this->subscribeStreamEvent($stream, Event::WRITE); + } + } + + /** + * {@inheritdoc} + */ + public function removeReadStream($stream) + { + $key = (int) $stream; + + if (isset($this->readListeners[$key])) { + unset($this->readListeners[$key]); + $this->unsubscribeStreamEvent($stream, Event::READ); + } + } + + /** + * {@inheritdoc} + */ + public function removeWriteStream($stream) + { + $key = (int) $stream; + + if (isset($this->writeListeners[$key])) { + unset($this->writeListeners[$key]); + $this->unsubscribeStreamEvent($stream, Event::WRITE); + } + } + + /** + * {@inheritdoc} + */ + public function removeStream($stream) + { + $key = (int) $stream; + + if (isset($this->streamEvents[$key])) { + $this->streamEvents[$key]->free(); + + unset( + $this->streamFlags[$key], + $this->streamEvents[$key], + $this->readListeners[$key], + $this->writeListeners[$key] + ); + } + } + + /** + * {@inheritdoc} + */ + public function addTimer($interval, callable $callback) + { + $timer = new Timer($this, $interval, $callback, false); + + $this->scheduleTimer($timer); + + return $timer; + } + + /** + * {@inheritdoc} + */ + public function addPeriodicTimer($interval, callable $callback) + { + $timer = new Timer($this, $interval, $callback, true); + + $this->scheduleTimer($timer); + + return $timer; + } + + /** + * {@inheritdoc} + */ + public function cancelTimer(TimerInterface $timer) + { + if ($this->isTimerActive($timer)) { + $this->timerEvents[$timer]->free(); + $this->timerEvents->detach($timer); + } + } + + /** + * {@inheritdoc} + */ + public function isTimerActive(TimerInterface $timer) + { + return $this->timerEvents->contains($timer); + } + + /** + * {@inheritdoc} + */ + public function nextTick(callable $listener) + { + $this->nextTickQueue->add($listener); + } + + /** + * {@inheritdoc} + */ + public function tick() + { + $this->nextTickQueue->tick(); + + // @-suppression: https://github.com/reactphp/react/pull/234#discussion-diff-7759616R226 + @$this->eventBase->loop(EventBase::LOOP_ONCE | EventBase::LOOP_NONBLOCK); + } + + /** + * {@inheritdoc} + */ + public function run() + { + $this->running = true; + + while ($this->running) { + $this->nextTickQueue->tick(); + + if (!$this->streamEvents && !$this->timerEvents->count()) { + break; + } + + // @-suppression: https://github.com/reactphp/react/pull/234#discussion-diff-7759616R226 + @$this->eventBase->loop(EventBase::LOOP_ONCE); + } + } + + /** + * {@inheritdoc} + */ + public function stop() + { + $this->running = false; + } + + /** + * Schedule a timer for execution. + * + * @param TimerInterface $timer + */ + private function scheduleTimer(TimerInterface $timer) + { + $flags = Event::TIMEOUT; + + if ($timer->isPeriodic()) { + $flags |= Event::PERSIST; + } + + $event = new Event($this->eventBase, -1, $flags, $this->timerCallback, $timer); + $this->timerEvents[$timer] = $event; + + $event->add($timer->getInterval()); + } + + /** + * Create a new ext-event Event object, or update the existing one. + * + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE + */ + private function subscribeStreamEvent($stream, $flag) + { + $key = (int) $stream; + + if (isset($this->streamEvents[$key])) { + $event = $this->streamEvents[$key]; + $flags = ($this->streamFlags[$key] |= $flag); + + $event->del(); + $event->set($this->eventBase, $stream, Event::PERSIST | $flags, $this->streamCallback); + } else { + $event = new Event($this->eventBase, $stream, Event::PERSIST | $flag, $this->streamCallback); + + $this->streamEvents[$key] = $event; + $this->streamFlags[$key] = $flag; + } + + $event->add(); + } + + /** + * Update the ext-event Event object for this stream to stop listening to + * the given event type, or remove it entirely if it's no longer needed. + * + * @param stream $stream + * @param integer $flag Event::READ or Event::WRITE + */ + private function unsubscribeStreamEvent($stream, $flag) + { + $key = (int) $stream; + + $flags = $this->streamFlags[$key] &= ~$flag; + + if (0 === $flags) { + $this->removeStream($stream); + + return; + } + + $event = $this->streamEvents[$key]; + + $event->del(); + $event->set($this->eventBase, $stream, Event::PERSIST | $flags, $this->streamCallback); + $event->add(); + } + + /** + * Create a callback used as the target of timer events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createTimerCallback() + { + $this->timerCallback = function ($_, $_, $timer) { + call_user_func($timer->getCallback(), $timer); + + if (!$timer->isPeriodic() && $this->isTimerActive($timer)) { + $this->cancelTimer($timer); + } + }; + } + + /** + * Create a callback used as the target of stream events. + * + * A reference is kept to the callback for the lifetime of the loop + * to prevent "Cannot destroy active lambda function" fatal error from + * the event extension. + */ + private function createStreamCallback() + { + $this->streamCallback = function ($stream, $flags) { + $key = (int) $stream; + + if (Event::READ === (Event::READ & $flags) && isset($this->readListeners[$key])) { + call_user_func($this->readListeners[$key], $stream, $this); + } + + if (Event::WRITE === (Event::WRITE & $flags) && isset($this->writeListeners[$key])) { + call_user_func($this->writeListeners[$key], $stream, $this); + } + }; + } +} diff --git a/src/React/EventLoop/LibEventLoop.php b/src/React/EventLoop/LibEventLoop.php index 599cb63b..88288003 100644 --- a/src/React/EventLoop/LibEventLoop.php +++ b/src/React/EventLoop/LibEventLoop.php @@ -2,6 +2,8 @@ namespace React\EventLoop; +use Event; +use EventBase; use React\EventLoop\Tick\NextTickQueue; use React\EventLoop\Timer\Timer; use React\EventLoop\Timer\TimerInterface; diff --git a/tests/React/Tests/EventLoop/ExtEventLoopTest.php b/tests/React/Tests/EventLoop/ExtEventLoopTest.php new file mode 100644 index 00000000..a9570b40 --- /dev/null +++ b/tests/React/Tests/EventLoop/ExtEventLoopTest.php @@ -0,0 +1,59 @@ +markTestSkipped('libevent tests skipped on linux due to linux epoll issues.'); + } + + if (!extension_loaded('event')) { + $this->markTestSkipped('ext-event tests skipped because ext-event is not installed.'); + } + + return new ExtEventLoop(); + } + + public function createStream() + { + // Use a FIFO on linux to get around lack of support for disk-based file + // descriptors when using the EPOLL back-end. + if ('Linux' === PHP_OS) { + $this->fifoPath = tempnam(sys_get_temp_dir(), 'react-'); + + unlink($this->fifoPath); + + posix_mkfifo($this->fifoPath, 0600); + + $stream = fopen($this->fifoPath, 'r+'); + + // ext-event (as of 1.8.1) does not yet support in-memory temporary + // streams. Setting maxmemory:0 and performing a write forces PHP to + // back this temporary stream with a real file. + // + // This problem is mentioned at https://bugs.php.net/bug.php?id=64652&edit=3 + // but remains unresolved (despite that issue being closed). + } else { + $stream = fopen('php://temp/maxmemory:0', 'r+'); + + fwrite($stream, 'x'); + ftruncate($stream, 0); + } + + return $stream; + } + + public function writeToStream($stream, $content) + { + if ('Linux' !== PHP_OS) { + return parent::writeToStream($stream, $content); + } + + fwrite($stream, $content); + } +} diff --git a/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php b/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php new file mode 100644 index 00000000..a7a6d005 --- /dev/null +++ b/tests/React/Tests/EventLoop/Timer/ExtEventTimerTest.php @@ -0,0 +1,17 @@ +markTestSkipped('ext-event tests skipped because ext-event is not installed.'); + } + + return new ExtEventLoop(); + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy