FdServerTest.php 15 KB

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