Skip to content

Commit 90d1e0b

Browse files
committed
Look up errno based on errstr when listening for connections fails
1 parent 52f23bb commit 90d1e0b

File tree

3 files changed

+52
-19
lines changed

3 files changed

+52
-19
lines changed

src/SocketServer.php

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -113,30 +113,45 @@ public static function accept($socket)
113113
// stream_socket_accept(): accept failed: Connection timed out
114114
$error = \error_get_last();
115115
$errstr = \preg_replace('#.*: #', '', $error['message']);
116-
117-
// Go through list of possible error constants to find matching errno.
118-
// @codeCoverageIgnoreStart
119-
$errno = 0;
120-
if (\function_exists('socket_strerror')) {
121-
foreach (\get_defined_constants(false) as $name => $value) {
122-
if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) {
123-
$errno = $value;
124-
$errstr .= ' (' . \substr($name, 7) . ')';
125-
break;
126-
}
127-
}
128-
}
129-
// @codeCoverageIgnoreEnd
116+
$errno = self::errno($errstr);
130117

131118
throw new \RuntimeException(
132-
'Unable to accept new connection: ' . $errstr,
119+
'Unable to accept new connection: ' . $errstr . self::errconst($errno),
133120
$errno
134121
);
135122
}
136123

137124
return $newSocket;
138125
}
139126

127+
/**
128+
* [Internal] Returns errno value for given errstr
129+
*
130+
* The errno and errstr values describes the type of error that has been
131+
* encountered. This method tries to look up the given errstr and find a
132+
* matching errno value which can be useful to provide more context to error
133+
* messages. It goes through the list of known errno constants when
134+
* ext-sockets is available to find an errno matching the given errstr.
135+
*
136+
* @param string $errstr
137+
* @return int errno value (e.g. value of `SOCKET_ECONNREFUSED`) or 0 if not found
138+
* @internal
139+
* @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/errno with permission
140+
* @codeCoverageIgnore
141+
*/
142+
public static function errno($errstr)
143+
{
144+
if (\function_exists('socket_strerror')) {
145+
foreach (\get_defined_constants(false) as $name => $value) {
146+
if (\strpos($name, 'SOCKET_E') === 0 && \socket_strerror($value) === $errstr) {
147+
return $value;
148+
}
149+
}
150+
}
151+
152+
return 0;
153+
}
154+
140155
/**
141156
* [Internal] Returns errno constant name for given errno value
142157
*

src/TcpServer.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ public function __construct($uri, LoopInterface $loop = null, array $context = a
175175
\stream_context_create(array('socket' => $context + array('backlog' => 511)))
176176
);
177177
if (false === $this->master) {
178+
if ($errno === 0) {
179+
// PHP does not seem to report errno, so match errno from errstr
180+
// @link https://3v4l.org/3qOBl
181+
$errno = SocketServer::errno($errstr);
182+
}
183+
178184
throw new \RuntimeException(
179185
'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno),
180186
$errno

tests/TcpServerTest.php

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public function testConstructWithoutLoopAssignsLoopAutomatically()
5151
public function testServerEmitsConnectionEventForNewConnection()
5252
{
5353
$client = stream_socket_client('tcp://localhost:'.$this->port);
54+
assert($client !== false);
5455

5556
$server = $this->server;
5657
$promise = new Promise(function ($resolve) use ($server) {
@@ -70,6 +71,7 @@ public function testConnectionWithManyClients()
7071
$client1 = stream_socket_client('tcp://localhost:'.$this->port);
7172
$client2 = stream_socket_client('tcp://localhost:'.$this->port);
7273
$client3 = stream_socket_client('tcp://localhost:'.$this->port);
74+
assert($client1 !== false && $client2 !== false && $client3 !== false);
7375

7476
$this->server->on('connection', $this->expectCallableExactly(3));
7577
$this->tick();
@@ -80,6 +82,7 @@ public function testConnectionWithManyClients()
8082
public function testDataEventWillNotBeEmittedWhenClientSendsNoData()
8183
{
8284
$client = stream_socket_client('tcp://localhost:'.$this->port);
85+
assert($client !== false);
8386

8487
$mock = $this->expectCallableNever();
8588

@@ -150,6 +153,7 @@ public function testGetAddressAfterCloseReturnsNull()
150153
public function testLoopWillEndWhenServerIsClosedAfterSingleConnection()
151154
{
152155
$client = stream_socket_client('tcp://localhost:' . $this->port);
156+
assert($client !== false);
153157

154158
// explicitly unset server because we only accept a single connection
155159
// and then already call close()
@@ -203,6 +207,7 @@ public function testDataWillBeEmittedInMultipleChunksWhenClientSendsExcessiveAmo
203207
public function testConnectionDoesNotEndWhenClientDoesNotClose()
204208
{
205209
$client = stream_socket_client('tcp://localhost:'.$this->port);
210+
assert($client !== false);
206211

207212
$mock = $this->expectCallableNever();
208213

@@ -236,7 +241,7 @@ public function testCtorAddsResourceToLoop()
236241
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
237242
$loop->expects($this->once())->method('addReadStream');
238243

239-
$server = new TcpServer(0, $loop);
244+
new TcpServer(0, $loop);
240245
}
241246

242247
public function testResumeWithoutPauseIsNoOp()
@@ -316,7 +321,7 @@ public function testEmitsErrorWhenAcceptListenerFails()
316321
public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $exception)
317322
{
318323
if (defined('HHVM_VERSION')) {
319-
$this->markTestSkipped('not supported on HHVM');
324+
$this->markTestSkipped('Not supported on HHVM');
320325
}
321326

322327
$this->assertEquals('Unable to accept new connection: ' . socket_strerror(SOCKET_ETIMEDOUT) . ' (ETIMEDOUT)', $exception->getMessage());
@@ -328,9 +333,16 @@ public function testListenOnBusyPortThrows()
328333
if (DIRECTORY_SEPARATOR === '\\') {
329334
$this->markTestSkipped('Windows supports listening on same port multiple times');
330335
}
336+
if (defined('HHVM_VERSION')) {
337+
$this->markTestSkipped('Not supported on HHVM');
338+
}
331339

332-
$this->setExpectedException('RuntimeException');
333-
$another = new TcpServer($this->port, $this->loop);
340+
$this->setExpectedException(
341+
'RuntimeException',
342+
'Failed to listen on "tcp://127.0.0.1:' . $this->port . '": ' . (function_exists('socket_strerror') ? socket_strerror(SOCKET_EADDRINUSE) . ' (EADDRINUSE)' : 'Address already in use'),
343+
defined('SOCKET_EADDRINUSE') ? SOCKET_EADDRINUSE : 0
344+
);
345+
new TcpServer($this->port, $this->loop);
334346
}
335347

336348
/**

0 commit comments

Comments
 (0)