Skip to content

Commit 1432d63

Browse files
authored
Merge pull request #43 from clue-labs/windows
Improve Windows support (async connections and Unix domain sockets)
2 parents 0c30a2f + 3b6f7c3 commit 1432d63

4 files changed

Lines changed: 55 additions & 39 deletions

File tree

src/Factory.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ public function createClient($address, $timeout = null)
3030
$socket->connectTimeout($address, $timeout);
3131
$socket->setBlocking(true);
3232
}
33-
}
34-
catch (Exception $e) {
33+
} catch (Exception $e) {
3534
$socket->close();
3635
throw $e;
3736
}
@@ -59,8 +58,7 @@ public function createServer($address)
5958
if ($socket->getType() === SOCK_STREAM) {
6059
$socket->listen();
6160
}
62-
}
63-
catch (Exception $e) {
61+
} catch (Exception $e) {
6462
$socket->close();
6563
throw $e;
6664
}

src/Socket.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,9 @@ public function connectTimeout($address, $timeout)
141141

142142
// socket is already connected immediately?
143143
return $this;
144-
}
145-
catch (Exception $e) {
146-
// non-blocking connect() should be EINPROGRESS => otherwise re-throw
147-
if ($e->getCode() !== SOCKET_EINPROGRESS) {
144+
} catch (Exception $e) {
145+
// non-blocking connect() should be EINPROGRESS (or EWOULDBLOCK on Windows) => otherwise re-throw
146+
if ($e->getCode() !== SOCKET_EINPROGRESS && $e->getCode() !== SOCKET_EWOULDBLOCK) {
148147
throw $e;
149148
}
150149

tests/FactoryTest.php

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,7 @@ public function testCreateClientTcp4UnboundFails()
9494
{
9595
try {
9696
$this->factory->createClient('tcp://localhost:2');
97-
}
98-
catch (Exception $e) {
97+
} catch (Exception $e) {
9998
if ($e->getCode() === SOCKET_ECONNREFUSED) {
10099
return;
101100
}
@@ -160,13 +159,18 @@ public function testCreateServerUdp4Random()
160159
*/
161160
public function testCreateServerUnix()
162161
{
163-
$path = '/tmp/randomfactorytests.sock';
162+
if (DIRECTORY_SEPARATOR !== '\\') {
163+
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'test-' . md5(microtime(true)) . '.sock';
164+
} else {
165+
// create test socket in local directory on Windows
166+
$path = 'test-' . md5(microtime(true)) . '.sock';
167+
}
164168

165169
$socket = $this->factory->createServer('unix://' . $path);
166170

167171
$this->assertInstanceOf('Socket\Raw\Socket', $socket);
168172

169-
unlink($path);
173+
$this->assertTrue(unlink($path), 'Unable to remove temporary socket ' . $path);
170174
}
171175

172176
/**
@@ -179,8 +183,7 @@ public function testCreateServerUnixFailInvalidPath()
179183

180184
try {
181185
$this->factory->createServer('unix://' . $path);
182-
}
183-
catch (Exception $e) {
186+
} catch (Exception $e) {
184187
return;
185188
}
186189

@@ -192,7 +195,10 @@ public function testCreateServerUnixFailInvalidPath()
192195
*/
193196
public function testCreateServerUdg()
194197
{
195-
// skip if not unix
198+
if (DIRECTORY_SEPARATOR === '\\') {
199+
// https://blogs.msdn.microsoft.com/commandline/2017/12/19/af_unix-comes-to-windows/
200+
$this->markTestSkipped('Unix datagram sockets not supported on Windows');
201+
}
196202

197203
$path = '/tmp/randomfactorytests.sock';
198204

@@ -207,8 +213,7 @@ public function testCreateServerIcmp4()
207213
{
208214
try {
209215
$socket = $this->factory->createServer('icmp://0.0.0.0');
210-
}
211-
catch (Exception $e) {
216+
} catch (Exception $e) {
212217
if ($e->getCode() === SOCKET_EPERM) {
213218
// skip if not root
214219
return $this->markTestSkipped('No access to ICMPv4 socket (only root can do so)');
@@ -226,8 +231,7 @@ public function testCreateServerIcmp6()
226231
{
227232
try {
228233
$socket = $this->factory->createServer('icmp6://[::1]');
229-
}
230-
catch (Exception $e) {
234+
} catch (Exception $e) {
231235
if ($e->getCode() === SOCKET_EPERM) {
232236
// skip if not root
233237
return $this->markTestSkipped('No access to ICMPv6 socket (only root can do so)');
@@ -287,6 +291,11 @@ public function testCreateUnix()
287291
*/
288292
public function testCreateUdg()
289293
{
294+
if (DIRECTORY_SEPARATOR === '\\') {
295+
// https://blogs.msdn.microsoft.com/commandline/2017/12/19/af_unix-comes-to-windows/
296+
$this->markTestSkipped('Unix datagram sockets not supported on Windows');
297+
}
298+
290299
$socket = $this->factory->createUdg();
291300

292301
$this->assertInstanceOf('Socket\Raw\Socket', $socket);
@@ -296,8 +305,7 @@ public function testCreateIcmp4()
296305
{
297306
try {
298307
$socket = $this->factory->createIcmp4();
299-
}
300-
catch (Exception $e) {
308+
} catch (Exception $e) {
301309
if ($e->getCode() === SOCKET_EPERM) {
302310
// skip if not root
303311
return $this->markTestSkipped('No access to ICMPv4 socket (only root can do so)');
@@ -315,8 +323,7 @@ public function testCreateIcmp6()
315323
{
316324
try {
317325
$socket = $this->factory->createIcmp6();
318-
}
319-
catch (Exception $e) {
326+
} catch (Exception $e) {
320327
if ($e->getCode() === SOCKET_EPERM) {
321328
// skip if not root
322329
return $this->markTestSkipped('No access to ICMPv6 socket (only root can do so)');
@@ -353,8 +360,7 @@ public function testCreateInvalid()
353360
{
354361
try {
355362
$this->factory->create(0, 1, 2);
356-
}
357-
catch (Exception $e) {
363+
} catch (Exception $e) {
358364
return;
359365
}
360366
$this->fail();
@@ -365,6 +371,10 @@ public function testCreateInvalid()
365371
*/
366372
public function testCreatePair()
367373
{
374+
if (DIRECTORY_SEPARATOR === '\\') {
375+
$this->markTestSkipped('Unix socket pair not supported on Windows');
376+
}
377+
368378
$sockets = $this->factory->createPair(AF_UNIX, SOCK_STREAM, 0);
369379

370380
$this->assertCount(2, $sockets);
@@ -379,8 +389,7 @@ public function testCreatePairInvalid()
379389
{
380390
try {
381391
$this->factory->createPair(0, 1, 2);
382-
}
383-
catch (Exception $e) {
392+
} catch (Exception $e) {
384393
return;
385394
}
386395
$this->fail();
@@ -473,8 +482,7 @@ public function testCreateFromStringInvalid()
473482
$address = 'invalid://whatever';
474483
try {
475484
$this->factory->createFromString($address, $scheme);
476-
}
477-
catch (Exception $e) {
485+
} catch (Exception $e) {
478486
return;
479487
}
480488
$this->fail('Creating socket for invalid scheme should fail');

tests/SocketTest.php

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,7 @@ public function testConnectFailUnbound()
6363
try {
6464
$this->factory->createClient('localhost:2');
6565
$this->fail('Expected connection to fail');
66-
}
67-
catch (Exception $e) {
66+
} catch (Exception $e) {
6867
$this->assertEquals(SOCKET_ECONNREFUSED, $e->getCode());
6968
}
7069
}
@@ -83,10 +82,12 @@ public function testConnectAsyncGoogle()
8382
try {
8483
$this->assertSame($socket, $socket->connect('www.google.com:80'));
8584
$this->fail('Calling connect() succeeded immediately');
86-
}
87-
catch (Exception $e) {
88-
// non-blocking connect() should be EINPROGRESS
89-
$this->assertEquals(SOCKET_EINPROGRESS, $e->getCode());
85+
} catch (Exception $e) {
86+
// non-blocking connect() should be EINPROGRESS or EWOULDBLOCK on Windows
87+
$this->assertEquals(
88+
DIRECTORY_SEPARATOR !== '\\' ? SOCKET_EINPROGRESS : SOCKET_EWOULDBLOCK,
89+
$e->getCode()
90+
);
9091

9192
// connection should be established within 5.0 seconds
9293
$this->assertTrue($socket->selectWrite(5.0));
@@ -100,6 +101,10 @@ public function testConnectAsyncGoogle()
100101

101102
public function testConnectAsyncFailUnbound()
102103
{
104+
if (PHP_OS !== 'Linux') {
105+
$this->markTestSkipped('Only Linux is known to refuse connections to unbound addresses');
106+
}
107+
103108
$socket = $this->factory->createTcp4();
104109

105110
$this->assertInstanceOf('Socket\Raw\Socket', $socket);
@@ -109,8 +114,7 @@ public function testConnectAsyncFailUnbound()
109114
try {
110115
$this->assertSame($socket, $socket->connect('localhost:2'));
111116
$this->fail('Calling connect() succeeded immediately');
112-
}
113-
catch (Exception $e) {
117+
} catch (Exception $e) {
114118
// non-blocking connect() should be EINPROGRESS
115119
$this->assertEquals(SOCKET_EINPROGRESS, $e->getCode());
116120

@@ -139,7 +143,8 @@ public function testSelectFloat()
139143
$this->assertFalse($socket->selectRead(0.5));
140144
$time = microtime(true) - $time;
141145

142-
$this->assertGreaterThan(0.5, $time);
146+
// check timer interval, but add some grace time to cope with inaccurate timers
147+
$this->assertGreaterThan(0.48, $time);
143148
$this->assertLessThan(1.0, $time);
144149
}
145150

@@ -176,6 +181,10 @@ public function testConnectTimeoutFailTimeout()
176181

177182
public function testConnectTimeoutFailUnbound()
178183
{
184+
if (PHP_OS !== 'Linux') {
185+
$this->markTestSkipped('Only Linux is known to refuse connections to unbound addresses');
186+
}
187+
179188
$socket = $this->factory->createTcp4();
180189

181190
$this->setExpectedException('Socket\Raw\Exception', null, SOCKET_ECONNREFUSED);
@@ -225,7 +234,9 @@ public function testServerNonBlockingAcceptClient(Socket $server)
225234
// create local client connected to the given server
226235
$client = $this->factory->createClient($server->getSockName());
227236

228-
// client connected, so we can not accept() this socket
237+
// client connected, so we should be able to accept() this socket immediately
238+
// on Windows, there appears to be a race condition, so first wait for server to be ready
239+
$server->selectRead(0.1);
229240
$peer = $server->accept();
230241

231242
// peer should be writable right away

0 commit comments

Comments
 (0)