diff --git a/src/TcpConnector.php b/src/TcpConnector.php index 23aba725..9e0a8bc6 100644 --- a/src/TcpConnector.php +++ b/src/TcpConnector.php @@ -99,14 +99,27 @@ public function connect($uri) // The following hack looks like the only way to // detect connection refused errors with PHP's stream sockets. if (false === \stream_socket_get_name($stream, true)) { - // actual socket errno and errstr can only be retrieved when ext-sockets is available (see tests) + // If we reach this point, we know the connection is dead, but we don't know the underlying error condition. // @codeCoverageIgnoreStart if (\function_exists('socket_import_stream')) { + // actual socket errno and errstr can be retrieved with ext-sockets on PHP 5.4+ $socket = \socket_import_stream($stream); $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR); $errstr = \socket_strerror($errno); + } elseif (\PHP_OS === 'Linux') { + // Linux reports socket errno and errstr again when trying to write to the dead socket. + // Suppress error reporting to get error message below and close dead socket before rejecting. + // This is only known to work on Linux, Mac and Windows are known to not support this. + @\fwrite($stream, \PHP_EOL); + $error = \error_get_last(); + + // fwrite(): send of 2 bytes failed with errno=111 Connection refused + \preg_match('/errno=(\d+) (.+)/', $error['message'], $m); + $errno = isset($m[1]) ? (int) $m[1] : 0; + $errstr = isset($m[2]) ? $m[2] : $error['message']; } else { - $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNRESET : 111; + // Not on Linux and ext-sockets not available? Too bad. + $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111; $errstr = 'Connection refused?'; } // @codeCoverageIgnoreEnd diff --git a/tests/TcpConnectorTest.php b/tests/TcpConnectorTest.php index 4b25ee6a..ee5b480e 100644 --- a/tests/TcpConnectorTest.php +++ b/tests/TcpConnectorTest.php @@ -103,21 +103,21 @@ public function connectionToTcpServerShouldFailIfFileDescriptorsAreExceeded() /** @test */ public function connectionToInvalidNetworkShouldFailWithUnreachableError() { - if (!defined('SOCKET_ENETUNREACH') || !function_exists('socket_import_stream')) { - $this->markTestSkipped('Test requires ext-socket on PHP 5.4+'); + if (PHP_OS !== 'Linux' && !function_exists('socket_import_stream')) { + $this->markTestSkipped('Test requires either Linux or ext-sockets on PHP 5.4+'); } + $enetunreach = defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101; + // try to find an unreachable network by trying a couple of private network addresses - $errno = 0; $errstr = ''; - for ($i = 0; $i < 20; ++$i) { + $errno = 0; + $errstr = ''; + for ($i = 0; $i < 20 && $errno !== $enetunreach; ++$i) { $address = 'tcp://192.168.' . mt_rand(0, 255) . '.' . mt_rand(1, 254) . ':8123'; $client = @stream_socket_client($address, $errno, $errstr, 0.1 * $i); - if ($errno === SOCKET_ENETUNREACH) { - break; - } } - if ($client || $errno !== SOCKET_ENETUNREACH) { - $this->markTestSkipped('Expected error ' . SOCKET_ENETUNREACH . ' but got ' . $errno . ' (' . $errstr . ') for ' . $address); + if ($client || $errno !== $enetunreach) { + $this->markTestSkipped('Expected error ' . $enetunreach . ' but got ' . $errno . ' (' . $errstr . ') for ' . $address); } $loop = Factory::create(); @@ -127,8 +127,8 @@ public function connectionToInvalidNetworkShouldFailWithUnreachableError() $this->setExpectedException( 'RuntimeException', - 'Connection to ' . $address . ' failed: ' . socket_strerror(SOCKET_ENETUNREACH), - SOCKET_ENETUNREACH + 'Connection to ' . $address . ' failed: ' . (function_exists('socket_strerror') ? socket_strerror($enetunreach) : 'Network is unreachable'), + $enetunreach ); Block\await($promise, $loop, self::TIMEOUT); }