Skip to content

Commit 6aaae22

Browse files
committed
Support listening on existing file descriptors (FDs) with SocketServer
1 parent 9909831 commit 6aaae22

File tree

8 files changed

+82
-17
lines changed

8 files changed

+82
-17
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ To listen on a Unix domain socket (UDS) path, you MUST prefix the URI with the
380380
$socket = new React\Socket\SocketServer('unix:///tmp/server.sock');
381381
```
382382

383+
In order to listen on an existing file descriptor (FD) number, you MUST prefix
384+
the URI with `php://fd/` like this:
385+
386+
```php
387+
$socket = new React\Socket\SocketServer('php://fd/3');
388+
```
389+
383390
If the given URI is invalid, does not contain a port, any other scheme or if it
384391
contains a hostname, it will throw an `InvalidArgumentException`:
385392

examples/01-echo-server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
//
1616
// $ php examples/01-echo-server.php unix:///tmp/server.sock
1717
// $ nc -U /tmp/server.sock
18+
//
19+
// You can also use systemd socket activation and listen on an inherited file descriptor:
20+
//
21+
// $ systemd-socket-activate -l 8000 php examples/01-echo-server.php php://fd/3
1822

1923
require __DIR__ . '/../vendor/autoload.php';
2024

examples/02-chat-server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
//
1616
// $ php examples/02-chat-server.php unix:///tmp/server.sock
1717
// $ nc -U /tmp/server.sock
18+
//
19+
// You can also use systemd socket activation and listen on an inherited file descriptor:
20+
//
21+
// $ systemd-socket-activate -l 8000 php examples/02-chat-server.php php://fd/3
1822

1923
require __DIR__ . '/../vendor/autoload.php';
2024

examples/03-http-server.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
//
2929
// $ php examples/03-http-server.php unix:///tmp/server.sock
3030
// $ nc -U /tmp/server.sock
31+
//
32+
// You can also use systemd socket activation and listen on an inherited file descriptor:
33+
//
34+
// $ systemd-socket-activate -l 8000 php examples/03-http-server.php php://fd/3
3135

3236
require __DIR__ . '/../vendor/autoload.php';
3337

src/FdServer.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,16 @@ final class FdServer extends EventEmitter implements ServerInterface
7070
* See the exception message and code for more details about the actual error
7171
* condition.
7272
*
73-
* @param int $fd
73+
* @param int|string $fd FD number such as `3` or as URL in the form of `php://fd/3`
7474
* @param ?LoopInterface $loop
7575
* @throws \InvalidArgumentException if the listening address is invalid
7676
* @throws \RuntimeException if listening on this address fails (already in use etc.)
7777
*/
7878
public function __construct($fd, LoopInterface $loop = null)
7979
{
80+
if (\preg_match('#^php://fd/(\d+)$#', $fd, $m)) {
81+
$fd = (int) $m[1];
82+
}
8083
if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) {
8184
throw new \InvalidArgumentException('Invalid FD number given');
8285
}

src/SocketServer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public function __construct($uri, array $context = array(), LoopInterface $loop
4848

4949
if ($scheme === 'unix') {
5050
$server = new UnixServer($uri, $loop, $context['unix']);
51+
} elseif ($scheme === 'php') {
52+
$server = new FdServer($uri, $loop);
5153
} else {
5254
if (preg_match('#^(?:\w+://)?\d+$#', $uri)) {
5355
throw new \InvalidArgumentException('Invalid URI given');

tests/FdServerTest.php

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public function testCtorAddsResourceToLoop()
1616
}
1717

1818
$socket = stream_socket_server('127.0.0.1:0');
19-
$fd = $this->getFdFromResource($socket);
19+
$fd = self::getFdFromResource($socket);
2020

2121
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
2222
$loop->expects($this->once())->method('addReadStream');
@@ -33,14 +33,23 @@ public function testCtorThrowsForInvalidFd()
3333
new FdServer(-1, $loop);
3434
}
3535

36+
public function testCtorThrowsForInvalidUrl()
37+
{
38+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
39+
$loop->expects($this->never())->method('addReadStream');
40+
41+
$this->setExpectedException('InvalidArgumentException');
42+
new FdServer('tcp://127.0.0.1:8080', $loop);
43+
}
44+
3645
public function testCtorThrowsForUnknownFd()
3746
{
3847
if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
3948
$this->markTestSkipped('Not supported on your platform');
4049
}
4150

4251
$socket = stream_socket_server('127.0.0.1:0');
43-
$fd = $this->getFdFromResource($socket);
52+
$fd = self::getFdFromResource($socket);
4453
fclose($socket);
4554

4655
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
@@ -61,7 +70,7 @@ public function testCtorThrowsIfFdIsAFileAndNotASocket()
6170
}
6271

6372
$tmpfile = tmpfile();
64-
$fd = $this->getFdFromResource($tmpfile);
73+
$fd = self::getFdFromResource($tmpfile);
6574

6675
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
6776
$loop->expects($this->never())->method('addReadStream');
@@ -83,7 +92,7 @@ public function testCtorThrowsIfFdIsAConnectedSocketInsteadOfServerSocket()
8392
$socket = stream_socket_server('tcp://127.0.0.1:0');
8493
$client = stream_socket_client('tcp://' . stream_socket_get_name($socket, false));
8594

86-
$fd = $this->getFdFromResource($client);
95+
$fd = self::getFdFromResource($client);
8796

8897
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
8998
$loop->expects($this->never())->method('addReadStream');
@@ -103,7 +112,7 @@ public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv4Socket()
103112
}
104113

105114
$socket = stream_socket_server('127.0.0.1:0');
106-
$fd = $this->getFdFromResource($socket);
115+
$fd = self::getFdFromResource($socket);
107116

108117
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
109118

@@ -112,6 +121,22 @@ public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv4Socket()
112121
$this->assertEquals('tcp://' . stream_socket_get_name($socket, false), $server->getAddress());
113122
}
114123

124+
public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv4SocketGivenAsUrlToFd()
125+
{
126+
if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
127+
$this->markTestSkipped('Not supported on your platform');
128+
}
129+
130+
$socket = stream_socket_server('127.0.0.1:0');
131+
$fd = self::getFdFromResource($socket);
132+
133+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
134+
135+
$server = new FdServer('php://fd/' . $fd, $loop);
136+
137+
$this->assertEquals('tcp://' . stream_socket_get_name($socket, false), $server->getAddress());
138+
}
139+
115140
public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv6Socket()
116141
{
117142
if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
@@ -123,7 +148,7 @@ public function testGetAddressReturnsSameAddressAsOriginalSocketForIpv6Socket()
123148
$this->markTestSkipped('Listening on IPv6 not supported');
124149
}
125150

126-
$fd = $this->getFdFromResource($socket);
151+
$fd = self::getFdFromResource($socket);
127152

128153
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
129154

@@ -144,7 +169,7 @@ public function testGetAddressReturnsSameAddressAsOriginalSocketForUnixDomainSoc
144169
$this->markTestSkipped('Listening on Unix domain socket (UDS) not supported');
145170
}
146171

147-
$fd = $this->getFdFromResource($socket);
172+
$fd = self::getFdFromResource($socket);
148173

149174
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
150175

@@ -160,7 +185,7 @@ public function testGetAddressReturnsNullAfterClose()
160185
}
161186

162187
$socket = stream_socket_server('127.0.0.1:0');
163-
$fd = $this->getFdFromResource($socket);
188+
$fd = self::getFdFromResource($socket);
164189

165190
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
166191

@@ -177,7 +202,7 @@ public function testCloseRemovesResourceFromLoop()
177202
}
178203

179204
$socket = stream_socket_server('127.0.0.1:0');
180-
$fd = $this->getFdFromResource($socket);
205+
$fd = self::getFdFromResource($socket);
181206

182207
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
183208
$loop->expects($this->once())->method('removeReadStream');
@@ -193,7 +218,7 @@ public function testCloseTwiceRemovesResourceFromLoopOnce()
193218
}
194219

195220
$socket = stream_socket_server('127.0.0.1:0');
196-
$fd = $this->getFdFromResource($socket);
221+
$fd = self::getFdFromResource($socket);
197222

198223
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
199224
$loop->expects($this->once())->method('removeReadStream');
@@ -210,7 +235,7 @@ public function testResumeWithoutPauseIsNoOp()
210235
}
211236

212237
$socket = stream_socket_server('127.0.0.1:0');
213-
$fd = $this->getFdFromResource($socket);
238+
$fd = self::getFdFromResource($socket);
214239

215240
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
216241
$loop->expects($this->once())->method('addReadStream');
@@ -226,7 +251,7 @@ public function testPauseRemovesResourceFromLoop()
226251
}
227252

228253
$socket = stream_socket_server('127.0.0.1:0');
229-
$fd = $this->getFdFromResource($socket);
254+
$fd = self::getFdFromResource($socket);
230255

231256
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
232257
$loop->expects($this->once())->method('removeReadStream');
@@ -242,7 +267,7 @@ public function testPauseAfterPauseIsNoOp()
242267
}
243268

244269
$socket = stream_socket_server('127.0.0.1:0');
245-
$fd = $this->getFdFromResource($socket);
270+
$fd = self::getFdFromResource($socket);
246271

247272
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
248273
$loop->expects($this->once())->method('removeReadStream');
@@ -259,7 +284,7 @@ public function testServerEmitsConnectionEventForNewConnection()
259284
}
260285

261286
$socket = stream_socket_server('127.0.0.1:0');
262-
$fd = $this->getFdFromResource($socket);
287+
$fd = self::getFdFromResource($socket);
263288

264289
$client = stream_socket_client('tcp://' . stream_socket_get_name($socket, false));
265290

@@ -289,7 +314,7 @@ public function testEmitsErrorWhenAcceptListenerFails()
289314
}));
290315

291316
$socket = stream_socket_server('127.0.0.1:0');
292-
$fd = $this->getFdFromResource($socket);
317+
$fd = self::getFdFromResource($socket);
293318

294319
$server = new FdServer($fd, $loop);
295320

@@ -333,7 +358,7 @@ public function testEmitsTimeoutErrorWhenAcceptListenerFails(\RuntimeException $
333358
* @throws \UnderflowException
334359
* @copyright Copyright (c) 2018 Christian Lück, taken from https://github.com/clue/fd with permission
335360
*/
336-
private function getFdFromResource($resource)
361+
public static function getFdFromResource($resource)
337362
{
338363
$stat = @fstat($resource);
339364
if (!isset($stat['ino']) || $stat['ino'] === 0) {

tests/SocketServerTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class SocketServerTest extends TestCase
1717
public function testConstructWithoutLoopAssignsLoopAutomatically()
1818
{
1919
$socket = new SocketServer('127.0.0.1:0');
20+
$socket->close();
2021

2122
$ref = new \ReflectionProperty($socket, 'server');
2223
$ref->setAccessible(true);
@@ -117,6 +118,21 @@ public function testConstructorThrowsForExistingUnixPath()
117118
}
118119
}
119120

121+
public function testConstructWithExistingFileDescriptorReturnsSameAddressAsOriginalSocketForIpv4Socket()
122+
{
123+
if (!is_dir('/dev/fd') || defined('HHVM_VERSION')) {
124+
$this->markTestSkipped('Not supported on your platform');
125+
}
126+
127+
$socket = stream_socket_server('127.0.0.1:0');
128+
$fd = FdServerTest::getFdFromResource($socket);
129+
130+
$server = new SocketServer('php://fd/' . $fd);
131+
$server->pause();
132+
133+
$this->assertEquals('tcp://' . stream_socket_get_name($socket, false), $server->getAddress());
134+
}
135+
120136
public function testEmitsErrorWhenUnderlyingTcpServerEmitsError()
121137
{
122138
$loop = Factory::create();

0 commit comments

Comments
 (0)