FdServerTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. <?php
  2. namespace React\Tests\Socket;
  3. use React\Promise\Promise;
  4. use React\Socket\ConnectionInterface;
  5. use React\Socket\FdServer;
  6. class FdServerTest extends TestCase
  7. {
  8. public function testCtorAddsResourceToLoop()
  9. {
  10. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  11. $this->markTestSkipped('Not supported on your platform');
  12. }
  13. $fd = self::getNextFreeFd();
  14. $socket = stream_socket_server('127.0.0.1:0');
  15. assert($socket !== false);
  16. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  17. $loop->expects($this->once())->method('addReadStream');
  18. new FdServer($fd, $loop);
  19. }
  20. public function testCtorThrowsForInvalidFd()
  21. {
  22. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  23. $loop->expects($this->never())->method('addReadStream');
  24. $this->setExpectedException(
  25. 'InvalidArgumentException',
  26. 'Invalid FD number given (EINVAL)',
  27. defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
  28. );
  29. new FdServer(-1, $loop);
  30. }
  31. public function testCtorThrowsForInvalidUrl()
  32. {
  33. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  34. $loop->expects($this->never())->method('addReadStream');
  35. $this->setExpectedException(
  36. 'InvalidArgumentException',
  37. 'Invalid FD number given (EINVAL)',
  38. defined('SOCKET_EINVAL') ? SOCKET_EINVAL : (defined('PCNTL_EINVAL') ? PCNTL_EINVAL : 22)
  39. );
  40. new FdServer('tcp://127.0.0.1:8080', $loop);
  41. }
  42. public function testCtorThrowsForUnknownFdWithoutCallingCustomErrorHandler()
  43. {
  44. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  45. $this->markTestSkipped('Not supported on your platform');
  46. }
  47. $fd = self::getNextFreeFd();
  48. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  49. $loop->expects($this->never())->method('addReadStream');
  50. $error = null;
  51. set_error_handler(function ($_, $errstr) use (&$error) {
  52. $error = $errstr;
  53. });
  54. $this->setExpectedException(
  55. 'RuntimeException',
  56. 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EBADF) . ' (EBADF)' : 'Bad file descriptor'),
  57. defined('SOCKET_EBADF') ? SOCKET_EBADF : 9
  58. );
  59. try {
  60. new FdServer($fd, $loop);
  61. restore_error_handler();
  62. } catch (\Exception $e) {
  63. restore_error_handler();
  64. $this->assertNull($error);
  65. throw $e;
  66. }
  67. }
  68. public function testCtorThrowsIfFdIsAFileAndNotASocket()
  69. {
  70. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  71. $this->markTestSkipped('Not supported on your platform');
  72. }
  73. $fd = self::getNextFreeFd();
  74. $tmpfile = tmpfile();
  75. assert($tmpfile !== false);
  76. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  77. $loop->expects($this->never())->method('addReadStream');
  78. $this->setExpectedException(
  79. 'RuntimeException',
  80. 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_ENOTSOCK) : 'Not a socket') . ' (ENOTSOCK)',
  81. defined('SOCKET_ENOTSOCK') ? SOCKET_ENOTSOCK : 88
  82. );
  83. new FdServer($fd, $loop);
  84. }
  85. public function testCtorThrowsIfFdIsAConnectedSocketInsteadOfServerSocket()
  86. {
  87. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  88. $this->markTestSkipped('Not supported on your platform');
  89. }
  90. $socket = stream_socket_server('tcp://127.0.0.1:0');
  91. $fd = self::getNextFreeFd();
  92. $client = stream_socket_client('tcp://' . stream_socket_get_name($socket, false));
  93. assert($client !== false);
  94. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  95. $loop->expects($this->never())->method('addReadStream');
  96. $this->setExpectedException(
  97. 'RuntimeException',
  98. 'Failed to listen on FD ' . $fd . ': ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EISCONN) : 'Socket is connected') . ' (EISCONN)',
  99. defined('SOCKET_EISCONN') ? SOCKET_EISCONN : 106
  100. );
  101. new FdServer($fd, $loop);
  102. }
  103. public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv4Socket()
  104. {
  105. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  106. $this->markTestSkipped('Not supported on your platform');
  107. }
  108. $fd = self::getNextFreeFd();
  109. $socket = stream_socket_server('127.0.0.1:0');
  110. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  111. $server = new FdServer($fd, $loop);
  112. $this->assertEquals('tcp://' . stream_socket_get_name($socket, false), $server->getAddress());
  113. }
  114. public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv4SocketGivenAsUrlToFd()
  115. {
  116. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  117. $this->markTestSkipped('Not supported on your platform');
  118. }
  119. $fd = self::getNextFreeFd();
  120. $socket = stream_socket_server('127.0.0.1:0');
  121. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  122. $server = new FdServer('php://fd/' . $fd, $loop);
  123. $this->assertEquals('tcp://' . stream_socket_get_name($socket, false), $server->getAddress());
  124. }
  125. public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv6Socket()
  126. {
  127. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  128. $this->markTestSkipped('Not supported on your platform');
  129. }
  130. $fd = self::getNextFreeFd();
  131. $socket = @stream_socket_server('[::1]:0');
  132. if ($socket === false) {
  133. $this->markTestSkipped('Listening on IPv6 not supported');
  134. }
  135. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  136. $server = new FdServer($fd, $loop);
  137. $port = preg_replace('/.*:/', '', stream_socket_get_name($socket, false));
  138. $this->assertEquals('tcp://[::1]:' . $port, $server->getAddress());
  139. }
  140. public function testGetAddressReturnsSameAddressAsOriginalSocketForUnixDomainSocket()
  141. {
  142. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  143. $this->markTestSkipped('Not supported on your platform');
  144. }
  145. $fd = self::getNextFreeFd();
  146. $socket = @stream_socket_server($this->getRandomSocketUri());
  147. if ($socket === false) {
  148. $this->markTestSkipped('Listening on Unix domain socket (UDS) not supported');
  149. }
  150. assert(is_resource($socket));
  151. unlink(str_replace('unix://', '', stream_socket_get_name($socket, false)));
  152. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  153. $server = new FdServer($fd, $loop);
  154. $this->assertEquals('unix://' . stream_socket_get_name($socket, false), $server->getAddress());
  155. }
  156. public function testGetAddressReturnsNullAfterClose()
  157. {
  158. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  159. $this->markTestSkipped('Not supported on your platform');
  160. }
  161. $fd = self::getNextFreeFd();
  162. $socket = stream_socket_server('127.0.0.1:0');
  163. assert($socket !== false);
  164. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  165. $server = new FdServer($fd, $loop);
  166. $server->close();
  167. $this->assertNull($server->getAddress());
  168. }
  169. public function testCloseRemovesResourceFromLoop()
  170. {
  171. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  172. $this->markTestSkipped('Not supported on your platform');
  173. }
  174. $fd = self::getNextFreeFd();
  175. $socket = stream_socket_server('127.0.0.1:0');
  176. assert($socket !== false);
  177. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  178. $loop->expects($this->once())->method('removeReadStream');
  179. $server = new FdServer($fd, $loop);
  180. $server->close();
  181. }
  182. public function testCloseTwiceRemovesResourceFromLoopOnce()
  183. {
  184. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  185. $this->markTestSkipped('Not supported on your platform');
  186. }
  187. $fd = self::getNextFreeFd();
  188. $socket = stream_socket_server('127.0.0.1:0');
  189. assert($socket !== false);
  190. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  191. $loop->expects($this->once())->method('removeReadStream');
  192. $server = new FdServer($fd, $loop);
  193. $server->close();
  194. $server->close();
  195. }
  196. public function testResumeWithoutPauseIsNoOp()
  197. {
  198. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  199. $this->markTestSkipped('Not supported on your platform');
  200. }
  201. $fd = self::getNextFreeFd();
  202. $socket = stream_socket_server('127.0.0.1:0');
  203. assert($socket !== false);
  204. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  205. $loop->expects($this->once())->method('addReadStream');
  206. $server = new FdServer($fd, $loop);
  207. $server->resume();
  208. }
  209. public function testPauseRemovesResourceFromLoop()
  210. {
  211. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  212. $this->markTestSkipped('Not supported on your platform');
  213. }
  214. $fd = self::getNextFreeFd();
  215. $socket = stream_socket_server('127.0.0.1:0');
  216. assert($socket !== false);
  217. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  218. $loop->expects($this->once())->method('removeReadStream');
  219. $server = new FdServer($fd, $loop);
  220. $server->pause();
  221. }
  222. public function testPauseAfterPauseIsNoOp()
  223. {
  224. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  225. $this->markTestSkipped('Not supported on your platform');
  226. }
  227. $fd = self::getNextFreeFd();
  228. $socket = stream_socket_server('127.0.0.1:0');
  229. assert($socket !== false);
  230. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  231. $loop->expects($this->once())->method('removeReadStream');
  232. $server = new FdServer($fd, $loop);
  233. $server->pause();
  234. $server->pause();
  235. }
  236. public function testServerEmitsConnectionEventForNewConnection()
  237. {
  238. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  239. $this->markTestSkipped('Not supported on your platform');
  240. }
  241. $fd = self::getNextFreeFd();
  242. $socket = stream_socket_server('127.0.0.1:0');
  243. assert($socket !== false);
  244. $client = stream_socket_client('tcp://' . stream_socket_get_name($socket, false));
  245. $server = new FdServer($fd);
  246. $promise = new Promise(function ($resolve) use ($server) {
  247. $server->on('connection', $resolve);
  248. });
  249. $connection = \React\Async\await(\React\Promise\Timer\timeout($promise, 1.0));
  250. /**
  251. * @var ConnectionInterface $connection
  252. */
  253. $this->assertInstanceOf('React\Socket\ConnectionInterface', $connection);
  254. fclose($client);
  255. $connection->close();
  256. $server->close();
  257. }
  258. public function testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler()
  259. {
  260. if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
  261. $this->markTestSkipped('Not supported on your platform');
  262. }
  263. $listener = null;
  264. $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
  265. $loop->expects($this->once())->method('addReadStream')->with($this->anything(), $this->callback(function ($cb) use (&$listener) {
  266. $listener = $cb;
  267. return true;
  268. }));
  269. $fd = self::getNextFreeFd();
  270. $socket = stream_socket_server('127.0.0.1:0');
  271. assert($socket !== false);
  272. $server = new FdServer($fd, $loop);
  273. $exception = null;
  274. $server->on('error', function ($e) use (&$exception) {
  275. $exception = $e;
  276. });
  277. $this->assertNotNull($listener);
  278. $socket = stream_socket_server('tcp://127.0.0.1:0');
  279. $error = null;
  280. set_error_handler(function ($_, $errstr) use (&$error) {
  281. $error = $errstr;
  282. });
  283. $time = microtime(true);
  284. $listener($socket);
  285. $time = microtime(true) - $time;
  286. restore_error_handler();
  287. $this->assertNull($error);
  288. $this->assertLessThan(1, $time);
  289. $this->assertInstanceOf('RuntimeException', $exception);
  290. assert($exception instanceof \RuntimeException);
  291. $this->assertStringStartsWith('Unable to accept new connection: ', $exception->getMessage());
  292. return $exception;
  293. }
  294. /**
  295. * @param \RuntimeException $e
  296. * @requires extension sockets
  297. * @depends testEmitsErrorWhenAcceptListenerFailsWithoutCallingCustomErrorHandler
  298. */
  299. public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception)
  300. {
  301. $this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage());
  302. $this->assertEquals(SOCKET_ETIMEDOUT, $exception->getCode());
  303. }
  304. /**
  305. * @return int
  306. * @throws \UnexpectedValueException
  307. * @throws \BadMethodCallException
  308. * @throws \UnderflowException
  309. * @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/fd with permission
  310. */
  311. public static function getNextFreeFd()
  312. {
  313. // open tmpfile to occupy next free FD temporarily
  314. $tmp = tmpfile();
  315. $dir = @scandir('/dev/fd');
  316. if ($dir === false) {
  317. throw new \BadMethodCallException('Not supported on your platform because /dev/fd is not readable');
  318. }
  319. $stat = fstat($tmp);
  320. $ino = (int) $stat['ino'];
  321. foreach ($dir as $file) {
  322. $stat = @stat('/dev/fd/' . $file);
  323. if (isset($stat['ino']) && $stat['ino'] === $ino) {
  324. return (int) $file;
  325. }
  326. }
  327. throw new \UnderflowException('Could not locate file descriptor for this resource');
  328. }
  329. private function getRandomSocketUri()
  330. {
  331. return "unix://" . sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid(rand(), true) . '.sock';
  332. }
  333. }